LispLoaderExtra

Short description

🧩 LispLoaderExtra v1.5.000 is a modeless AutoCAD and Civil 3D .NET loader for scanning filtering selecting loading and reloading LSP VLX FAS and DLL files from a structured CAD tool library.
🎛️ The plugin provides a dark themed WinForms interface with category and name filters search checkboxes status columns a live log pane and load controls.
🛡️ Files are loaded through the AutoCAD application context with isolated results so one failed tool does not have to stop the rest of the selected load sequence.

Image 000 11

Command:

Main window: LISPLOADEXTRA
Last session report: LISPLOADREPORT
Loader diagnostics: LISPLOADDIAG

Description:

🧩 LispLoaderExtra v1.5.000 is a modeless AutoCAD and Civil 3D .NET loader for scanning filtering selecting loading and reloading LSP VLX FAS and DLL files from a structured CAD tool library.
🎛️ The plugin provides a dark themed WinForms interface with category and name filters search checkboxes status columns a live log pane and load controls.
🛡️ Files are loaded through the AutoCAD application context with isolated results so one failed tool does not have to stop the rest of the selected load sequence.

Helper function: (if any)​

Comming soon …

Functionalities:

✅ Multi format scanning – discovers LSP VLX FAS and DLL files in the structured source tree.
✅ Modeless loader window – stays usable while AutoCAD remains active.
✅ Category and name filters – narrows the displayed tool collection.
✅ Text search – filters visible loader rows by search text.
✅ Checkbox selection – supports selective loading and Select All or None actions.
✅ Load and reload actions – process checked files or force reload the chosen set.
✅ Load Selected – loads highlighted grid rows and is also available through F3.
✅ Persistent checked state – stores checked file paths in ss checked.json between sessions.
✅ Automatic disk rescan – uses FileSystemWatcher with debounce when supported files change.
✅ Right click tools – provides load reload open folder open editor copy path and checkbox actions.
✅ Editor integration – opens source files in Notepad++ with a Notepad fallback.
✅ Live log panel – shows real time color coded loading messages with Store Log Clear and Auto Scroll.
✅ Status change flash – briefly highlights rows whose load status changed.
✅ Diagnostics command – searches loaded AutoCAD assemblies for LISP related invocation methods.

Result:

🎯 LispLoaderExtra v1.5.000 provides the described AutoCAD or Civil 3D workflow.
📌 The generated SEO description is based on the supplied source code or project files.

Images, animations etc.

Image 000 11
Pixel

XAML code:

<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:src="clr-namespace:Autodesk.Windows;assembly=AdWindows">

<src:RibbonToolTip x:Key="LispLoaderExtra">
<src:RibbonToolTip.ExpandedContent>
<StackPanel>
<TextBlock Background="AntiqueWhite" TextAlignment="Left">
<Bold>Function Syntax: LISPLOADEXTRA / LISPLOADREPORT / LISPLOADDIAG</Bold>
<LineBreak/>
<Bold>Version: 1.0 Date: 25.06.2026 8:46:25AM</Bold>
<LineBreak/>
<Bold>Version: 1.0 Date: 11.08.2026 3:38:21PM</Bold>
<LineBreak/>
<Hyperlink>N/A</Hyperlink>

<Bold><Run Foreground="DodgerBlue">Description</Run></Bold><LineBreak/>
<Run Foreground="White">🧩 LispLoaderExtra v1.5.000 is a modeless AutoCAD and Civil 3D .NET loader for scanning filtering selecting loading and reloading LSP VLX FAS and DLL files from a structured CAD tool library.</Run><LineBreak/>
<Run Foreground="White">🎛️ The plugin provides a dark themed WinForms interface with category and name filters search checkboxes status columns a live log pane and load controls.</Run><LineBreak/>
<Run Foreground="White">🛡️ Files are loaded through the AutoCAD application context with isolated results so one failed tool does not have to stop the rest of the selected load sequence.</Run><LineBreak/>
<Bold><Run Foreground="DodgerBlue">Function Syntax</Run></Bold><LineBreak/>
<Run Foreground="White">Main window: </Run><Bold><Run Foreground="Orange">LISPLOADEXTRA</Run></Bold><LineBreak/>
<Run Foreground="White">Last session report: </Run><Bold><Run Foreground="Orange">LISPLOADREPORT</Run></Bold><LineBreak/>
<Run Foreground="White">Loader diagnostics: </Run><Bold><Run Foreground="Orange">LISPLOADDIAG</Run></Bold><LineBreak/>
<Bold><Run Foreground="LimeGreen">Primary Workflow</Run></Bold><LineBreak/>
<Run Foreground="White">1. NETLOAD the compiled LispLoaderExtra DLL into AutoCAD.</Run><LineBreak/>
<Run Foreground="White">2. Run LISPLOADEXTRA to open the modeless loader window.</Run><LineBreak/>
<Run Foreground="White">3. The application scans the configured Help_File tree for supported program files.</Run><LineBreak/>
<Run Foreground="White">4. Filter by AutoCAD or Civil 3D category name or free text and check the files that should be processed.</Run><LineBreak/>
<Run Foreground="White">5. Use Load Reload or Load Selected to process the required files.</Run><LineBreak/>
<Run Foreground="White">6. Review per row status version information and the live color coded log.</Run><LineBreak/>
<Run Foreground="White">7. Use LISPLOADREPORT to review last session results or LISPLOADDIAG when loader API diagnostics are required.</Run><LineBreak/>
<Bold><Run Foreground="LimeGreen">Functionalities</Run></Bold><LineBreak/>
<Run Foreground="White">✅ </Run><Bold><Run Foreground="LimeGreen">Multi format scanning</Run></Bold><Run Foreground="White"> - discovers LSP VLX FAS and DLL files in the structured source tree.</Run><LineBreak/>
<Run Foreground="White">✅ </Run><Bold><Run Foreground="LimeGreen">Modeless loader window</Run></Bold><Run Foreground="White"> - stays usable while AutoCAD remains active.</Run><LineBreak/>
<Run Foreground="White">✅ </Run><Bold><Run Foreground="LimeGreen">Category and name filters</Run></Bold><Run Foreground="White"> - narrows the displayed tool collection.</Run><LineBreak/>
<Run Foreground="White">✅ </Run><Bold><Run Foreground="LimeGreen">Text search</Run></Bold><Run Foreground="White"> - filters visible loader rows by search text.</Run><LineBreak/>
<Run Foreground="White">✅ </Run><Bold><Run Foreground="LimeGreen">Checkbox selection</Run></Bold><Run Foreground="White"> - supports selective loading and Select All or None actions.</Run><LineBreak/>
<Run Foreground="White">✅ </Run><Bold><Run Foreground="LimeGreen">Load and reload actions</Run></Bold><Run Foreground="White"> - process checked files or force reload the chosen set.</Run><LineBreak/>
<Run Foreground="White">✅ </Run><Bold><Run Foreground="LimeGreen">Load Selected</Run></Bold><Run Foreground="White"> - loads highlighted grid rows and is also available through F3.</Run><LineBreak/>
<Run Foreground="White">✅ </Run><Bold><Run Foreground="LimeGreen">Persistent checked state</Run></Bold><Run Foreground="White"> - stores checked file paths in ss checked.json between sessions.</Run><LineBreak/>
<Run Foreground="White">✅ </Run><Bold><Run Foreground="LimeGreen">Automatic disk rescan</Run></Bold><Run Foreground="White"> - uses FileSystemWatcher with debounce when supported files change.</Run><LineBreak/>
<Run Foreground="White">✅ </Run><Bold><Run Foreground="LimeGreen">Right click tools</Run></Bold><Run Foreground="White"> - provides load reload open folder open editor copy path and checkbox actions.</Run><LineBreak/>
<Run Foreground="White">✅ </Run><Bold><Run Foreground="LimeGreen">Editor integration</Run></Bold><Run Foreground="White"> - opens source files in Notepad++ with a Notepad fallback.</Run><LineBreak/>
<Run Foreground="White">✅ </Run><Bold><Run Foreground="LimeGreen">Live log panel</Run></Bold><Run Foreground="White"> - shows real time color coded loading messages with Store Log Clear and Auto Scroll.</Run><LineBreak/>
<Run Foreground="White">✅ </Run><Bold><Run Foreground="LimeGreen">Status change flash</Run></Bold><Run Foreground="White"> - briefly highlights rows whose load status changed.</Run><LineBreak/>
<Run Foreground="White">✅ </Run><Bold><Run Foreground="LimeGreen">Diagnostics command</Run></Bold><Run Foreground="White"> - searches loaded AutoCAD assemblies for LISP related invocation methods.</Run><LineBreak/>
<Bold><Run Foreground="DarkRed">Important Notes</Run></Bold><LineBreak/>
<Run Foreground="White">⚠️ </Run><Bold><Run Foreground="DarkRed">Project version is 1.5.000.</Run></Bold><LineBreak/>
<Run Foreground="White">⚠️ </Run><Bold><Run Foreground="DarkRed">LISP VLX and FAS loading uses SendStringToExecute first then acedInvoke and reflection fallback strategies.</Run></Bold><LineBreak/>
<Run Foreground="White">⚠️ </Run><Bold><Run Foreground="DarkRed">A 30 second per file timeout is used by the current FileLoader implementation.</Run></Bold><LineBreak/>
<Run Foreground="White">⚠️ </Run><Bold><Run Foreground="DarkRed">DLL loading uses Assembly.LoadFrom and AutoCAD ExtensionLoader.</Run></Bold><LineBreak/>
<Run Foreground="White">⚠️ </Run><Bold><Run Foreground="DarkRed">Escape is swallowed by the modeless window to avoid passing the key into the AutoCAD viewport.</Run></Bold><LineBreak/>
<Bold><Run Foreground="Yellow">SEO Keywords</Run></Bold><LineBreak/>
<Run Foreground="Yellow">🏷️ AutoCAD  🏷️ Civil 3D  🏷️ LispLoaderExtra  🏷️ AutoLISP loader  🏷️ VLX loader  🏷️ FAS loader  🏷️ DLL loader  🏷️ NETLOAD  🏷️ WinForms  🏷️ FileSystemWatcher  🏷️ LISPLOADREPORT  🏷️ LISPLOADDIAG  🏷️ CAD automation  🏷️ Plugin manager</Run><LineBreak/>
</TextBlock>
            
<Grid>
<Image Source="Image_000.png" Stretch="Uniform"/>
</Grid>

</StackPanel>
</src:RibbonToolTip.ExpandedContent>
</src:RibbonToolTip>
</ResourceDictionary>

C# code:

// ===== FILE: DiskScanner.cs =====
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace LispLoaderExtra
{
    /// <summary>
    /// Scans the Help_File folder tree and returns ScannedFile entries.
    ///
    /// Structure:
    ///   AutoCAD:  Help_File\{MenuFolder}\{SubFolder}\*.{lsp,vlx,fas,dll}
    ///             MenuFolder must contain "_menu" in its name (case-insensitive)
    ///             SubFolder is one level deep inside the _menu folder
    ///             Files directly inside SubFolder are collected (no deeper recursion)
    ///
    ///   Civil 3D: Help_File\C3D\{C3D_*_menu}\{SubFolder}\*.{lsp,vlx,fas,dll}
    ///             Display name strips the "C3D_" prefix: C3D_Alignment_menu → Alignment_menu
    ///
    /// Exclusions (applied to every path segment below the base):
    ///   - Exact names (case-insensitive): ss, vlx_dll, vlx_fas, lispsloader
    ///   - Names starting with those tokens + "_" or "-" (e.g. ss_01, vlx_fas_2)
    ///   - Names starting with "@"
    ///
    /// Improvements over previous version:
    ///   - All string comparisons use net48-compatible overloads (no StringComparison on Contains)
    ///   - Split uses char[] instead of two-arg char overload (net48 compat)
    ///   - Results sorted: AutoCAD first, then Civil 3D; alphabetically within each group
    ///   - SafeGetDirectories / SafeGetFiles log errors instead of swallowing silently
    /// </summary>
    internal static class DiskScanner
    {
        internal static readonly string BaseFolder =
            @"C:\Users\mateid\OneDrive - DB E.C.O. Group\@C3D\@Dan_Templates\Acad\Support_files\Lisps\Help_File";

        private static readonly string C3DFolder =
            Path.Combine(BaseFolder, "C3D");

        private static readonly HashSet<string> ExcludedNames =
            new HashSet<string>(StringComparer.OrdinalIgnoreCase)
            { "ss", "vlx_dll", "vlx_fas", "lispsloader", "lispploaderextra" };

        private static readonly HashSet<string> SupportedExtensions =
            new HashSet<string>(StringComparer.OrdinalIgnoreCase)
            { ".lsp", ".vlx", ".fas", ".dll" };

        private static readonly char[] PathSeps = new char[] { '\\', '/' };

        // ── Public API ────────────────────────────────────────────────────────

        internal static List<ScannedFile> Scan()
        {
            var results = new List<ScannedFile>();
            if (!Directory.Exists(BaseFolder))
            {
                LogManager.Warn("DiskScanner", $"Base folder not found: {BaseFolder}");
                return results;
            }

            ScanAutoCAD(results);
            ScanC3D(results);

            // Sort: category first, then menu name, then file name
            results.Sort((a, b) =>
            {
                int c = string.Compare(a.Category, b.Category, StringComparison.OrdinalIgnoreCase);
                if (c != 0) return c;
                c = string.Compare(a.MenuName, b.MenuName, StringComparison.OrdinalIgnoreCase);
                if (c != 0) return c;
                return string.Compare(a.FileName, b.FileName, StringComparison.OrdinalIgnoreCase);
            });

            // Assign sequential row numbers after sort
            for (int i = 0; i < results.Count; i++)
                results[i].RowIndex = i + 1;

            LogManager.Info("DiskScanner",
                $"Scan complete: {results.Count} files " +
                $"(AutoCAD: {results.Count(f => f.Category == "AutoCAD")}, " +
                $"Civil 3D: {results.Count(f => f.Category == "Civil 3D")})");

            return results;
        }

        /// <summary>Returns distinct sorted menu names for the given category.</summary>
        internal static List<string> GetMenuNames(string category, List<ScannedFile> files)
        {
            return files
                .Where(f => category == "All" ||
                            string.Equals(f.Category, category, StringComparison.OrdinalIgnoreCase))
                .Select(f => f.MenuName)
                .Distinct(StringComparer.OrdinalIgnoreCase)
                .OrderBy(n => n, StringComparer.OrdinalIgnoreCase)
                .ToList();
        }

        // ── AutoCAD scan ──────────────────────────────────────────────────────
        // Pattern: Help_File \ {*_menu folder} \ {sub-folder} \ {files}

        private static void ScanAutoCAD(List<ScannedFile> results)
        {
            foreach (string menuDir in SafeGetDirectories(BaseFolder))
            {
                string menuName = Path.GetFileName(menuDir);

                // Must contain "_menu" — net48: use IndexOf, not Contains(str, comparison)
                if (menuName.IndexOf("_menu", StringComparison.OrdinalIgnoreCase) < 0) continue;
                if (string.Equals(menuName, "C3D", StringComparison.OrdinalIgnoreCase)) continue;
                if (IsExcluded(menuName)) continue;

                foreach (string subDir in SafeGetDirectories(menuDir))
                {
                    if (IsExcluded(Path.GetFileName(subDir))) continue;

                    foreach (string file in SafeGetFiles(subDir))
                    {
                        string ext = Path.GetExtension(file);
                        if (!SupportedExtensions.Contains(ext)) continue;
                        if (IsExcludedPath(file))               continue;

                        results.Add(new ScannedFile(
                            category:  "AutoCAD",
                            menuName:  menuName,
                            fileName:  Path.GetFileName(file),
                            extension: ext,
                            fullPath:  file));
                    }
                }
            }
        }

        // ── Civil 3D scan ─────────────────────────────────────────────────────
        // Pattern: Help_File \ C3D \ {C3D_*_menu folder} \ {sub-folder} \ {files}

        private static void ScanC3D(List<ScannedFile> results)
        {
            if (!Directory.Exists(C3DFolder)) return;

            foreach (string menuDir in SafeGetDirectories(C3DFolder))
            {
                string menuName = Path.GetFileName(menuDir);
                if (menuName.IndexOf("_menu", StringComparison.OrdinalIgnoreCase) < 0) continue;
                if (IsExcluded(menuName)) continue;

                // Strip "C3D_" prefix for display: C3D_Alignment_menu → Alignment_menu
                string displayName = menuName.StartsWith("C3D_", StringComparison.OrdinalIgnoreCase)
                    ? menuName.Substring(4)
                    : menuName;

                foreach (string subDir in SafeGetDirectories(menuDir))
                {
                    if (IsExcluded(Path.GetFileName(subDir))) continue;

                    foreach (string file in SafeGetFiles(subDir))
                    {
                        string ext = Path.GetExtension(file);
                        if (!SupportedExtensions.Contains(ext)) continue;
                        if (IsExcludedPath(file))               continue;

                        results.Add(new ScannedFile(
                            category:  "Civil 3D",
                            menuName:  displayName,
                            fileName:  Path.GetFileName(file),
                            extension: ext,
                            fullPath:  file));
                    }
                }
            }
        }

        // ── Exclusion helpers ─────────────────────────────────────────────────

        private static bool IsExcluded(string folderName)
        {
            if (string.IsNullOrEmpty(folderName)) return false;
            if (folderName.StartsWith("@"))        return true;

            string low = folderName.ToLowerInvariant();
            foreach (string ex in ExcludedNames)
            {
                string exl = ex.ToLowerInvariant();
                if (low == exl ||
                    low.StartsWith(exl + "_", StringComparison.Ordinal) ||
                    low.StartsWith(exl + "-", StringComparison.Ordinal))
                    return true;
            }
            return false;
        }

        private static bool IsExcludedPath(string fullPath)
        {
            if (fullPath.Length <= BaseFolder.Length) return false;

            string rel = fullPath.Substring(BaseFolder.Length).TrimStart(PathSeps);

            // net48: Split(char[]) — not Split(char, char)
            foreach (string part in rel.Split(PathSeps))
            {
                if (IsExcluded(part)) return true;
            }
            return false;
        }

        // ── Safe I/O ─────────────────────────────────────────────────────────

        private static string[] SafeGetDirectories(string path)
        {
            try   { return Directory.GetDirectories(path); }
            catch (System.Exception ex)
            {
                LogManager.Warn("DiskScanner", $"Cannot read directories: {path} — {ex.Message}");
                return new string[0];
            }
        }

        private static string[] SafeGetFiles(string path)
        {
            try   { return Directory.GetFiles(path); }
            catch (System.Exception ex)
            {
                LogManager.Warn("DiskScanner", $"Cannot read files: {path} — {ex.Message}");
                return new string[0];
            }
        }
    }
}


// ===== FILE: LoadSession.cs =====
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace LispLoaderExtra
{
    /// <summary>
    /// Collects all LoadResult entries for one plugin-load session.
    /// Provides summary statistics and formatted report text.
    /// Singleton — one session per AutoCAD load.
    /// </summary>
    internal static class LoadSession
    {
        private static readonly List<LoadResult> _results = new List<LoadResult>();
        private static DateTime _startTime;

        internal static IReadOnlyList<LoadResult> Results => _results.AsReadOnly();

        internal static void Begin()
        {
            _results.Clear();
            _startTime = DateTime.Now;
        }

        internal static void Add(LoadResult r)
        {
            _results.Add(r);
            // Mirror to log
            switch (r.Status)
            {
                case LoadStatus.OK:
                    if (r.Extension == ".dll" && !string.IsNullOrEmpty(r.ErrorMessage))
                    {
                        // DLL OK — ErrorMessage carries the version stamp
                        LogManager.Info("FileLoader",
                            $"[OK-DLL]  {r.FileName}  v{r.ErrorMessage}  ({r.Duration.TotalMilliseconds:0}ms)");
                        LogManager.Info("FileLoader", $"         {r.Path}");
                    }
                    else
                    {
                        LogManager.Info("FileLoader",
                            $"[OK-{r.TypeLabel,-3}]  {r.FileName}  ({r.Duration.TotalMilliseconds:0}ms)");
                        LogManager.Info("FileLoader", $"         {r.Path}");
                    }
                    break;
                case LoadStatus.Failed:
                    LogManager.Error("FileLoader", $"[FAIL-{r.TypeLabel,-3}] {r.FileName}");
                    LogManager.Error("FileLoader", $"         {r.ErrorMessage}");
                    LogManager.Error("FileLoader", $"         {r.Path}");
                    break;
                case LoadStatus.NotFound:
                    LogManager.Warn("FileLoader", $"[MISSING]  {r.Path}");
                    break;
                case LoadStatus.Skipped:
                    LogManager.Warn("FileLoader", $"[SKIP]     {r.Path}  (unsupported type)");
                    break;
            }
        }

        // ── Stats ─────────────────────────────────────────────────────────────

        internal static int TotalCount    => _results.Count;
        internal static int OkCount       => _results.Count(r => r.Status == LoadStatus.OK);
        internal static int FailCount     => _results.Count(r => r.Status == LoadStatus.Failed);
        internal static int NotFoundCount => _results.Count(r => r.Status == LoadStatus.NotFound);
        internal static int SkipCount     => _results.Count(r => r.Status == LoadStatus.Skipped);

        internal static int CountByType(string ext) =>
            _results.Count(r => r.Extension == ext && r.Status == LoadStatus.OK);

        internal static TimeSpan TotalDuration => DateTime.Now - _startTime;

        // ── Summary line (for command line) ───────────────────────────────────

        internal static string SummaryLine =>
            $"Load complete — " +
            $"OK: {OkCount}  " +
            $"FAILED: {FailCount}  " +
            $"NOT FOUND: {NotFoundCount}  " +
            $"SKIPPED: {SkipCount}  " +
            $"| {TotalDuration.TotalSeconds:0.0}s total";

        // ── Full text report (for clipboard / log) ────────────────────────────

        internal static string BuildTextReport()
        {
            var sb = new StringBuilder();
            string line = new string('─', 72);

            sb.AppendLine(line);
            sb.AppendLine($"  LispLoaderExtra — Load Report");
            sb.AppendLine($"  {DateTime.Now:dd.MM.yyyy  HH:mm:ss}");
            sb.AppendLine(line);
            sb.AppendLine($"  Total files:  {TotalCount}");
            sb.AppendLine($"  OK:           {OkCount}  " +
                          $"(LSP:{CountByType(".lsp")}  " +
                          $"VLX:{CountByType(".vlx")}  " +
                          $"FAS:{CountByType(".fas")}  " +
                          $"DLL:{CountByType(".dll")})");
            sb.AppendLine($"  Failed:       {FailCount}");
            sb.AppendLine($"  Not found:    {NotFoundCount}");
            sb.AppendLine($"  Skipped:      {SkipCount}");
            sb.AppendLine($"  Total time:   {TotalDuration.TotalSeconds:0.00}s");
            sb.AppendLine(line);

            // Errors first
            var errors = _results.Where(r => r.Status == LoadStatus.Failed).ToList();
            if (errors.Count > 0)
            {
                sb.AppendLine("  ERRORS:");
                foreach (var r in errors)
                {
                    sb.AppendLine($"    [{r.TypeLabel}] {r.FileName}");
                    sb.AppendLine($"           {r.Path}");
                    sb.AppendLine($"           {r.ErrorMessage}");
                }
                sb.AppendLine(line);
            }

            var missing = _results.Where(r => r.Status == LoadStatus.NotFound).ToList();
            if (missing.Count > 0)
            {
                sb.AppendLine("  NOT FOUND:");
                foreach (var r in missing)
                    sb.AppendLine($"    {r.Path}");
                sb.AppendLine(line);
            }

            sb.AppendLine("  ALL FILES:");
            foreach (var r in _results)
            {
                string status = r.Status switch {
                    LoadStatus.OK       => "OK     ",
                    LoadStatus.Failed   => "FAIL   ",
                    LoadStatus.NotFound => "MISSING",
                    LoadStatus.Skipped  => "SKIP   ",
                    _                   => "???????",
                };
                string extra = (r.Extension == ".dll" && r.Status == LoadStatus.OK && !string.IsNullOrEmpty(r.ErrorMessage))
                    ? $"  v{r.ErrorMessage}"
                    : "";
                sb.AppendLine($"    {status}  [{r.TypeLabel,-3}]  {r.FileName}  ({r.Duration.TotalMilliseconds:0}ms){extra}");
            }
            sb.AppendLine(line);

            return sb.ToString();
        }
    }
}


// ===== FILE: ConfigReader.cs =====
using System;
using System.Collections.Generic;
using System.IO;

namespace LispLoaderExtra
{
    internal static class ConfigReader
    {
        private const string Source = "ConfigReader";

        // Base folder for resolving relative paths listed in the config
        internal static readonly string BaseFolder =
            @"C:\Users\mateid\OneDrive - DB E.C.O. Group\@C3D\@Dan_Templates\Acad\Support_files\Lisps\Help_File";

        // Config file lives next to the log in the ss\ folder
        internal static readonly string ConfigFile =
            Path.Combine(LogManager.LogFolder, "LoaderConfig.txt");

        internal static IReadOnlyList<string> ReadPaths()
        {
            LogManager.Info(Source, $"Looking for config: {ConfigFile}");

            if (!File.Exists(ConfigFile))
            {
                LogManager.Error(Source, $"Config not found: {ConfigFile}");
                return Array.Empty<string>();
            }

            var paths = new List<string>();

            try
            {
                foreach (string raw in File.ReadAllLines(ConfigFile))
                {
                    string line = raw.Trim();
                    if (string.IsNullOrEmpty(line)) continue;
                    if (line.StartsWith("#"))        continue;
                    if (line.StartsWith(";"))        continue;

                    string resolved = Path.IsPathRooted(line)
                        ? line
                        : Path.Combine(BaseFolder, line);

                    paths.Add(resolved);
                }

                LogManager.Info(Source, $"Config loaded — {paths.Count} entries");
            }
            catch (System.Exception ex)
            {
                LogManager.Error(Source, $"Failed to read config: {ex.Message}");
            }

            return paths;
        }
    }
}


// ===== FILE: LoadResult.cs =====
using System;

namespace LispLoaderExtra
{
    /// <summary>
    /// The outcome of attempting to load a single file.
    /// </summary>
    internal enum LoadStatus
    {
        OK,         // loaded successfully
        Failed,     // file exists but load threw an error
        NotFound,   // file path does not exist on disk
        Skipped,    // unsupported extension
    }

    /// <summary>
    /// Immutable record of one file-load attempt.
    /// </summary>
    internal sealed class LoadResult
    {
        internal string     Path        { get; }
        internal string     Extension   { get; }   // .lsp / .vlx / .fas / .dll
        internal LoadStatus Status      { get; }
        internal string     ErrorMessage{ get; }   // empty string when OK
        internal TimeSpan   Duration    { get; }
        internal DateTime   Timestamp   { get; }

        internal LoadResult(
            string     path,
            string     extension,
            LoadStatus status,
            string     errorMessage,
            TimeSpan   duration)
        {
            Path         = path;
            Extension    = extension.ToLowerInvariant();
            Status       = status;
            ErrorMessage = errorMessage ?? string.Empty;
            Duration     = duration;
            Timestamp    = DateTime.Now;
        }

        // Convenience helpers
        internal string FileName   => System.IO.Path.GetFileName(Path);
        internal string TypeLabel  => Extension.TrimStart('.').ToUpperInvariant();   // LSP / VLX / FAS / DLL
        internal bool   IsOk       => Status == LoadStatus.OK;
        internal bool   HasError   => Status == LoadStatus.Failed;
    }
}


// ===== FILE: FileLoader.cs =====
// Alias System.Exception explicitly to avoid ambiguity with
// Autodesk.AutoCAD.Runtime.Exception which lives in the same namespace.
using SysEx = System.Exception;

using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Runtime;

namespace LispLoaderExtra
{
    /// <summary>
    /// Loads .lsp / .vlx / .fas / .dll files safely from ANY thread.
    ///
    /// THREAD-SAFETY — ROOT CAUSE OF v1.3 ACCESS VIOLATION
    /// ─────────────────────────────────────────────────────
    /// acedInvoke (P/Invoke into accore.dll) and ExtensionLoader.Load
    /// MUST run on AutoCAD's document-context thread.  In v1.3 the form is
    /// modeless (Show, not ShowDialog), so Load() was called from the WinForms
    /// UI thread → Access Violation at 96AFEB62h.
    ///
    /// FIX — ExecuteInApplicationContext
    /// ──────────────────────────────────
    /// Every load is posted to AutoCAD's own thread via:
    ///   Application.DocumentManager.ExecuteInApplicationContext(cb, null)
    /// A ManualResetEventSlim lets the UI thread block until the result is
    /// ready (30 s timeout) while the form remains repaint-responsive.
    ///
    /// LISP LOAD STRATEGIES (tried in order on the ACAD thread):
    ///   A. Document.SendStringToExecute("(load \"/path\")\n", false, false, false)
    ///      Safest: injects LISP into AutoCAD's own command evaluator.
    ///      Signature: (string commandString, bool activate, bool wrapUpInactiveDoc,
    ///                  bool echoCommand) — no 'wrap' named param.
    ///   B. acedInvoke P/Invoke from accore.dll
    ///   C. Reflection scan for acedInvoke in acmgd / accoremgd
    ///
    /// DLL LOAD:
    ///   Assembly.LoadFrom + ExtensionLoader.Load on ACAD thread.
    ///   Version stamp: "major.minor.build_[dd.mm.yyyy_hh.mm.ss]" (file creation time).
    /// </summary>
    internal static class FileLoader
    {
        // ── resbuf type codes ─────────────────────────────────────────────────
        private const short RTSTR = 5005;
        private const short RTLB  = 5016;
        private const short RTLE  = 5017;

        // ── P/Invoke — only safe on ACAD application-context thread ──────────
        [DllImport("accore.dll",
            CallingConvention = CallingConvention.Cdecl,
            EntryPoint = "acedInvoke")]
        private static extern int AcedInvoke(IntPtr args, out IntPtr result);

        private static bool _pinvokeAvailable = true;

        // ── Reflection cache ──────────────────────────────────────────────────
        private static MethodInfo? _reflectMethod;
        private static bool        _reflectSearched;
        private static readonly object _reflectLock = new object();

        // ═════════════════════════════════════════════════════════════════════
        // PUBLIC ENTRY — safe to call from any thread (UI, background, etc.)
        // ═════════════════════════════════════════════════════════════════════

        internal static LoadResult Load(string path)
        {
            if (string.IsNullOrWhiteSpace(path))
                return Make(path ?? "", "", LoadStatus.Skipped, "Empty path");

            path = path.Trim();
            string ext = Path.GetExtension(path).ToLowerInvariant();

            if (!File.Exists(path))
                return Make(path, ext, LoadStatus.NotFound, "File not found: " + path);

            // Post work to AutoCAD's application-context thread and block
            // until done.  ManualResetEventSlim avoids spinning.
            LoadResult? result = null;
            using var done = new ManualResetEventSlim(false);

            Application.DocumentManager.ExecuteInApplicationContext(
                delegate
                {
                    try
                    {
                        switch (ext)
                        {
                            case ".lsp":
                            case ".vlx":
                            case ".fas":
                                result = LoadLisp(path, ext);
                                break;
                            case ".dll":
                                result = LoadDll(path);
                                break;
                            default:
                                result = Make(path, ext, LoadStatus.Skipped,
                                             "Unsupported extension: " + ext);
                                break;
                        }
                    }
                    catch (SysEx ex)
                    {
                        result = Make(path, ext, LoadStatus.Failed,
                            "Unhandled error in application context: " + ex.Message);
                    }
                    finally
                    {
                        done.Set();
                    }
                },
                null);

            // 30 s timeout — graceful degradation if ACAD is waiting for modal input
            if (!done.Wait(TimeSpan.FromSeconds(30)))
            {
                LogManager.Warn("FileLoader",
                    $"Load timed out (30 s): {Path.GetFileName(path)}");
                return Make(path, ext, LoadStatus.Failed,
                    "Load timed out — AutoCAD may be waiting for user input.");
            }

            return result ?? Make(path, ext, LoadStatus.Failed, "No result returned.");
        }

        // ═════════════════════════════════════════════════════════════════════
        // LISP / VLX / FAS — runs on ACAD application-context thread
        // ═════════════════════════════════════════════════════════════════════

        private static LoadResult LoadLisp(string path, string ext)
        {
            var sw = Stopwatch.StartNew();
            try
            {
                Document? doc = Application.DocumentManager?.MdiActiveDocument;
                if (doc == null)
                    return Make(path, ext, LoadStatus.Failed,
                        "No active document — open a drawing first.", sw.Elapsed);

                // ── Strategy A: SendStringToExecute ───────────────────────────
                // Signature (AutoCAD 2010+):
                //   void SendStringToExecute(string commandString,
                //                            bool   activate,
                //                            bool   wrapUpInactiveDoc,
                //                            bool   echoCommand)
                // NOTE: named param is 'wrapUpInactiveDoc', NOT 'wrap'.
                // Use positional args to stay compatible across ACAD versions.
                try
                {
                    // LISP requires forward slashes in paths
                    string lispPath = path.Replace('\\', '/');
                    string lispExpr = "(load \"" + lispPath + "\")\n";

                    using (doc.LockDocument())
                    {
                        // positional: (commandString, activate, wrapUpInactiveDoc, echoCommand)
                        doc.SendStringToExecute(lispExpr, false, false, false);
                    }

                    sw.Stop();
                    LogManager.Info("FileLoader",
                        "[SendString] Loaded: " + Path.GetFileName(path));
                    return Make(path, ext, LoadStatus.OK, string.Empty, sw.Elapsed);
                }
                catch (SysEx ssEx)
                {
                    LogManager.Warn("FileLoader",
                        "SendStringToExecute failed for " + Path.GetFileName(path) +
                        ": " + ssEx.Message + " — trying acedInvoke.");
                }

                // ── Strategy B: P/Invoke acedInvoke ──────────────────────────
                if (_pinvokeAvailable)
                {
                    try
                    {
                        using (doc.LockDocument())
                        using (var rb = new ResultBuffer(
                            new TypedValue(RTLB),
                            new TypedValue(RTSTR, "load"),
                            new TypedValue(RTSTR, path),
                            new TypedValue(RTLE)))
                        {
                            AcedInvoke(rb.UnmanagedObject, out IntPtr _);
                        }
                        sw.Stop();
                        return Make(path, ext, LoadStatus.OK, string.Empty, sw.Elapsed);
                    }
                    catch (EntryPointNotFoundException epnf)
                    {
                        _pinvokeAvailable = false;
                        LogManager.Warn("FileLoader",
                            "acedInvoke entry not found: " + epnf.Message);
                    }
                    catch (DllNotFoundException dnf)
                    {
                        _pinvokeAvailable = false;
                        LogManager.Warn("FileLoader",
                            "accore.dll not found: " + dnf.Message);
                    }
                    catch (SysEx ex)
                    {
                        LogManager.Warn("FileLoader",
                            "acedInvoke threw: " + ex.Message + " — trying reflection.");
                    }
                }

                // ── Strategy C: Reflection ────────────────────────────────────
                MethodInfo? method = GetReflectMethod();
                if (method != null)
                {
                    try
                    {
                        using (doc.LockDocument())
                        using (var rb = new ResultBuffer(
                            new TypedValue(RTLB),
                            new TypedValue(RTSTR, "load"),
                            new TypedValue(RTSTR, path),
                            new TypedValue(RTLE)))
                        {
                            method.Invoke(null, new object?[] { rb, null });
                        }
                        sw.Stop();
                        return Make(path, ext, LoadStatus.OK, string.Empty, sw.Elapsed);
                    }
                    catch (SysEx ex)
                    {
                        sw.Stop();
                        return Make(path, ext, LoadStatus.Failed,
                            ex.InnerException?.Message ?? ex.Message, sw.Elapsed);
                    }
                }

                sw.Stop();
                return Make(path, ext, LoadStatus.Failed,
                    "All load strategies failed — run LISPLOADDIAG for details.", sw.Elapsed);
            }
            catch (SysEx ex)
            {
                sw.Stop();
                return Make(path, ext, LoadStatus.Failed, ex.Message, sw.Elapsed);
            }
        }

        // ═════════════════════════════════════════════════════════════════════
        // DLL — runs on ACAD application-context thread
        // ═════════════════════════════════════════════════════════════════════

        private static LoadResult LoadDll(string path)
        {
            var sw = Stopwatch.StartNew();
            try
            {
                System.Reflection.Assembly asm =
                    System.Reflection.Assembly.LoadFrom(path);
                ExtensionLoader.Load(path);
                sw.Stop();

                Version? verObj = asm.GetName().Version;
                string verNum = verObj != null
                    ? string.Format("{0}.{1}.{2:D3}", verObj.Major, verObj.Minor, verObj.Build)
                    : "?.?.???";

                string creationStamp;
                try   { creationStamp = File.GetCreationTime(path).ToString("dd.MM.yyyy_HH.mm.ss"); }
                catch { creationStamp = "??.??.????_??.??.??"; }

                // version stamp stored in ErrorMessage (re-used as info field for OK results)
                return Make(path, ".dll", LoadStatus.OK,
                    verNum + "_[" + creationStamp + "]", sw.Elapsed);
            }
            catch (SysEx ex)
            {
                sw.Stop();
                return Make(path, ".dll", LoadStatus.Failed, ex.Message, sw.Elapsed);
            }
        }

        // ═════════════════════════════════════════════════════════════════════
        // REFLECTION SCAN — cached after first successful search
        // ═════════════════════════════════════════════════════════════════════

        private static MethodInfo? GetReflectMethod()
        {
            if (_reflectSearched) return _reflectMethod;
            lock (_reflectLock)
            {
                if (_reflectSearched) return _reflectMethod;
                _reflectSearched = true;

                string[] targets = { "acmgd", "accoremgd", "acdbmgd", "acmgdui", "acapp" };

                foreach (System.Reflection.Assembly asm in
                         AppDomain.CurrentDomain.GetAssemblies())
                {
                    string asmName = asm.GetName().Name ?? "";
                    bool isTarget  = false;
                    foreach (string t in targets)
                        if (asmName.Equals(t, StringComparison.OrdinalIgnoreCase))
                        { isTarget = true; break; }
                    if (!isTarget) continue;

                    Type[] types;
                    try { types = asm.GetTypes(); } catch { continue; }

                    foreach (Type type in types)
                    {
                        MethodInfo[] methods;
                        try
                        {
                            methods = type.GetMethods(
                                BindingFlags.Public | BindingFlags.NonPublic |
                                BindingFlags.Static);
                        }
                        catch { continue; }

                        foreach (MethodInfo m in methods)
                        {
                            if (!m.Name.Equals("acedInvoke",
                                    StringComparison.OrdinalIgnoreCase)) continue;
                            ParameterInfo[] parms = m.GetParameters();
                            if (parms.Length < 1) continue;
                            if (parms[0].ParameterType != typeof(ResultBuffer) &&
                                parms[0].ParameterType != typeof(IntPtr)) continue;

                            LogManager.Info("FileLoader",
                                "acedInvoke found via reflection: " +
                                type.FullName + "." + m.Name);
                            _reflectMethod = m;
                            return m;
                        }
                    }
                }

                LogManager.Warn("FileLoader",
                    "acedInvoke not found via reflection — run LISPLOADDIAG.");
                return null;
            }
        }

        // ═════════════════════════════════════════════════════════════════════
        // FACTORY
        // ═════════════════════════════════════════════════════════════════════

        private static LoadResult Make(
            string path, string ext, LoadStatus status,
            string error, TimeSpan duration = default) =>
            new LoadResult(
                path,
                string.IsNullOrEmpty(ext) ? ".?" : ext,
                status, error, duration);
    }
}


// ===== FILE: VersionStamp.cs =====
using System;
using System.Linq;
using System.Reflection;

namespace LispLoaderExtra
{
    /// <summary>
    /// Prints the version banner whenever the plugin loads.
    ///
    /// Format:  v1.0.000 | built 12.06.2026 10:30:00
    ///
    /// Version   → AssemblyVersion set in &lt;Version&gt; in the .csproj
    /// Timestamp → AssemblyMetadata("BuildTimestamp", "dd.MM.yyyy_HH.mm.ss")
    ///             injected at compile time by the MSBuild SetBuildTimestamp target.
    ///             No generated .cs file required.
    /// </summary>
    internal static class VersionStamp
    {
        private const string Source = "LispLoader";

        internal static void Print()
        {
            Assembly asm = Assembly.GetExecutingAssembly();

            // ── Version ───────────────────────────────────────────────────────
            // Version.ToString(3) drops leading zeros from the build number:
            //   1.2.000 → "1.2.0" (wrong). Format manually to get "1.2.000".
            var v = asm.GetName().Version;
            string version = v != null
                ? v.Major + "." + v.Minor + "." + v.Build.ToString("D3")
                : "?.?.???";

            // ── Timestamp from AssemblyMetadata ───────────────────────────────
            // The .csproj target adds:
            //   [assembly: AssemblyMetadata("BuildTimestamp", "12.06.2026_10.30.00")]
            string raw = asm
                .GetCustomAttributes<AssemblyMetadataAttribute>()
                .FirstOrDefault(a => a.Key == "BuildTimestamp")
                ?.Value
                ?? "??.??.????_??.??.??";

            // raw = "12.06.2026_10.30.00"
            string[] halves = raw.Split('_');
            string date = halves.Length > 0 ? halves[0] : "??";                    // 12.06.2026
            string time = halves.Length > 1 ? halves[1].Replace('.', ':') : "??";  // 10:30:00

            string banner = $"v{version} | built {date} {time}";

            LogManager.Separator();
            LogManager.Info(Source, banner);
            LogManager.Separator();
        }
    }
}


// ===== FILE: LoaderWindow.cs =====
// No Autodesk.AutoCAD.Runtime using here — avoids Exception ambiguity.
// All ACAD calls go through FileLoader / LogManager / DiskScanner instead.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Windows.Forms;

namespace LispLoaderExtra
{
    /// <summary>
    /// Main loader window — type LISPLOADEXTRA in AutoCAD.
    ///
    /// v1.5 — compile-error fixes + new improvements
    /// ────────────────────────────────────────────────
    /// Fixes (from compiler errors reported on v1.4):
    ///   • 'Exception' ambiguity: no Autodesk.AutoCAD.Runtime using in this file;
    ///     all catches use System.Exception via the global alias.
    ///   • SendStringToExecute named-param 'wrap' → positional args in FileLoader.
    ///
    /// New in v1.5:
    ///   • "Load Selected" toolbar button + F3 shortcut
    ///   • Right-click context menu (Load / Reload / Open Folder / Copy Path /
    ///     Copy All Paths / Check / Uncheck)
    ///   • Progress bar in the status strip
    ///   • Column header tooltips
    ///   • DLL File cell shows version stamp in italic after load
    ///   • _loading guard prevents double-triggers
    ///   • FileSystemWatcher: auto-rescan when files change on disk
    ///   • Persist checked state to ss\checked.json between sessions
    ///   • "Open in Notepad++" right-click item (falls back to Notepad)
    ///   • Error-diff highlight: rows that changed FAIL→OK or OK→FAIL glow briefly
    ///
    /// Form behaviour:
    ///   FormBorderStyle.SizableToolWindow — small title bar, no taskbar entry
    ///   ProcessCmdKey swallows Escape (viewport-safe)
    ///   Modeless — stays open while AutoCAD is active
    ///   Live log panel (bottom pane) — real-time, colour-coded
    ///   Store Log / Clear / Auto-scroll
    /// </summary>
    internal sealed class LoaderWindow : Form
    {
        // ── Palette ───────────────────────────────────────────────────────────
        private static readonly Color BgDark    = Color.FromArgb(26,  26,  34);
        private static readonly Color BgPanel   = Color.FromArgb(36,  36,  48);
        private static readonly Color BgGrid    = Color.FromArgb(20,  20,  28);
        private static readonly Color BgSearch  = Color.FromArgb(44,  44,  58);
        private static readonly Color BgLog     = Color.FromArgb(16,  16,  22);
        private static readonly Color ColOk     = Color.FromArgb(80,  200, 100);
        private static readonly Color ColFail   = Color.FromArgb(220, 70,  70);
        private static readonly Color ColMiss   = Color.FromArgb(220, 155, 40);
        private static readonly Color ColText   = Color.FromArgb(210, 210, 222);
        private static readonly Color ColSub    = Color.FromArgb(120, 120, 138);
        private static readonly Color ColAccent = Color.FromArgb(80,  145, 215);
        private static readonly Color ColWarn   = Color.FromArgb(230, 160, 40);
        private static readonly Color ColError  = Color.FromArgb(220, 70,  70);
        private static readonly Color ColInfo   = Color.FromArgb(100, 200, 240);
        private static readonly Color ColLsp    = Color.FromArgb(100, 210, 120);
        private static readonly Color ColVlx    = Color.FromArgb(180, 130, 240);
        private static readonly Color ColFas    = Color.FromArgb(240, 185, 80);
        private static readonly Color ColDll    = Color.FromArgb(240, 205, 70);
        private static readonly Color ColC3D    = Color.FromArgb(110, 185, 240);
        private static readonly Color ColAcad   = Color.FromArgb(160, 230, 155);
        private static readonly Color ColFlash  = Color.FromArgb(255, 255, 100);  // diff-flash

        // ── Column indices ────────────────────────────────────────────────────
        private const int CI_Chk    = 0;
        private const int CI_Status = 1;
        private const int CI_Num    = 2;
        private const int CI_Cat    = 3;
        private const int CI_Menu   = 4;
        private const int CI_File   = 5;
        private const int CI_Ext    = 6;
        private const int CI_Ms     = 7;
        private const int CI_Path   = 8;

        // ── Checked-state persistence file ────────────────────────────────────
        private static readonly string CheckedFile =
            System.IO.Path.Combine(LogManager.LogFolder, "checked.json");

        // ── State ─────────────────────────────────────────────────────────────
        private List<ScannedFile> _allFiles  = new List<ScannedFile>();
        private List<ScannedFile> _visible   = new List<ScannedFile>();
        private bool _scanning  = false;
        private bool _loading   = false;

        // Tracks which files changed status in the last load (for flash)
        private readonly System.Collections.Generic.HashSet<string> _flashPaths
            = new System.Collections.Generic.HashSet<string>(
                StringComparer.OrdinalIgnoreCase);

        // FileSystemWatcher for auto-rescan
        private FileSystemWatcher? _watcher;
        private System.Windows.Forms.Timer? _rescanTimer;  // debounce

        // ── Controls — toolbar ────────────────────────────────────────────────
        private ComboBox  _cbCat      = null!;
        private ComboBox  _cbName     = null!;
        private TextBox   _txtSearch  = null!;
        private Button    _btnScan    = null!;
        private Button    _btnLoad    = null!;
        private Button    _btnReload  = null!;
        private Button    _btnLoadSel = null!;
        private Button    _btnAll     = null!;
        private Button    _btnNone    = null!;
        private Button    _btnReport  = null!;

        // ── Controls — main ───────────────────────────────────────────────────
        private DataGridView     _grid     = null!;
        private Label            _lblTitle = null!;
        private Label            _lblStatus = null!;
        private ProgressBar      _progress  = null!;
        private ToolTip          _tooltip   = null!;
        private ContextMenuStrip _ctxMenu   = null!;

        // ── Log panel ─────────────────────────────────────────────────────────
        private RichTextBox    _logBox        = null!;
        private Button         _btnStoreLog   = null!;
        private Button         _btnClearLog   = null!;
        private CheckBox       _chkAutoScroll = null!;
        private SplitContainer _split         = null!;

        // ── Constructor ───────────────────────────────────────────────────────

        internal LoaderWindow()
        {
            BuildLayout();

            LogManager.Subscribe(OnLogLine);

            // Seed log panel with prior session lines from ring-buffer
            foreach (string line in LogManager.GetRingSnapshot())
                AppendLogLine(line);

            LoadCheckedState();    // restore persisted checkbox ticks
            SetupWatcher();        // FileSystemWatcher for auto-rescan
            DoScan();
        }

        // ── Escape swallow ────────────────────────────────────────────────────

        protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
        {
            if (keyData == Keys.Escape) return true;   // consumed — safe in viewport
            return base.ProcessCmdKey(ref msg, keyData);
        }

        // ── Cleanup ───────────────────────────────────────────────────────────

        protected override void OnFormClosed(FormClosedEventArgs e)
        {
            LogManager.Unsubscribe(OnLogLine);
            SaveCheckedState();
            TearDownWatcher();
            base.OnFormClosed(e);
        }

        // ═════════════════════════════════════════════════════════════════════
        // LAYOUT
        // ═════════════════════════════════════════════════════════════════════

        private void BuildLayout()
        {
            Text            = "LispLoaderExtra";
            Size            = new Size(1300, 840);
            MinimumSize     = new Size(920, 600);
            StartPosition   = FormStartPosition.CenterScreen;
            BackColor       = BgDark;
            ForeColor       = ColText;
            Font            = new Font("Segoe UI", 9f);
            FormBorderStyle = FormBorderStyle.SizableToolWindow;
            ShowInTaskbar   = false;
            KeyPreview      = true;
            KeyDown        += OnKeyDown;

            _tooltip = new ToolTip {
                AutoPopDelay = 10000,
                InitialDelay = 400,
                ReshowDelay  = 200,
                ShowAlways   = true,
            };

            BuildContextMenu();
            BuildTitleBar();
            BuildToolbar();
            BuildStatusBar();       // Dock.Bottom — must be added before Fill panel
            BuildSplitArea();
        }

        // ── Title bar ─────────────────────────────────────────────────────────

        private void BuildTitleBar()
        {
            var panel = new Panel { Dock = DockStyle.Top, Height = 42, BackColor = BgPanel };
            _lblTitle = new Label {
                Text      = "LispLoaderExtra",
                Font      = new Font("Segoe UI", 13f, FontStyle.Bold),
                ForeColor = ColAccent,
                BackColor = Color.Transparent,
                Dock      = DockStyle.Fill,
                TextAlign = ContentAlignment.MiddleLeft,
                Padding   = new Padding(12, 0, 0, 0),
            };
            panel.Controls.Add(_lblTitle);
            Controls.Add(panel);
        }

        // ── Toolbar ───────────────────────────────────────────────────────────

        private void BuildToolbar()
        {
            var tb = new Panel { Dock = DockStyle.Top, Height = 42, BackColor = BgPanel };
            int x  = 10;

            tb.Controls.Add(Lbl("Cat:", x, 12, 28));  x += 30;
            _cbCat = Combo(x, 7, 100);
            _cbCat.Items.AddRange(new object[] { "All", "AutoCAD", "Civil 3D" });
            _cbCat.SelectedIndex = 0;
            _cbCat.SelectedIndexChanged += OnCatChanged;
            tb.Controls.Add(_cbCat);  x += 108;

            tb.Controls.Add(Lbl("Name:", x, 12, 40));  x += 42;
            _cbName = Combo(x, 7, 190);
            _cbName.SelectedIndexChanged += OnNameChanged;
            tb.Controls.Add(_cbName);  x += 198;

            tb.Controls.Add(Lbl("🔍", x, 11, 20));  x += 22;
            _txtSearch = new TextBox {
                Location    = new Point(x, 8),
                Size        = new Size(150, 26),
                BackColor   = BgSearch,
                ForeColor   = ColText,
                Font        = new Font("Segoe UI", 9f),
                BorderStyle = BorderStyle.FixedSingle,
            };
            _txtSearch.TextChanged += (s, e) => ApplyFilters();
            tb.Controls.Add(_txtSearch);  x += 158;

            x += 8;
            _btnScan    = Btn("🔍 Scan",      ColAccent,                       80,  x, tb); x += 88;
            _btnLoad    = Btn("▶ Load",        Color.FromArgb(35, 120, 55),     78,  x, tb); x += 86;
            _btnReload  = Btn("🔄 Reload",     Color.FromArgb(35, 95,  125),    82,  x, tb); x += 90;
            _btnLoadSel = Btn("▶ Load Sel.",   Color.FromArgb(80, 100, 40),     90,  x, tb); x += 98;

            x += 6;
            _btnAll  = Btn("☑ All",  Color.FromArgb(55, 55, 70), 60, x, tb); x += 68;
            _btnNone = Btn("☐ None", Color.FromArgb(55, 55, 70), 60, x, tb);

            _btnReport = Btn("📊 Report", Color.FromArgb(75, 55, 110), 96, 0, tb);
            _btnReport.Anchor   = AnchorStyles.Top | AnchorStyles.Right;
            _btnReport.Location = new Point(tb.Width - 108, 6);
            tb.Resize += (s, e) => _btnReport.Location = new Point(tb.Width - 108, 6);
            tb.Controls.Add(_btnReport);

            _btnScan.Click    += (s, e) => DoScan();
            _btnLoad.Click    += (s, e) => DoLoad(LoadScope.Checked,  reload: false);
            _btnReload.Click  += (s, e) => DoLoad(LoadScope.Checked,  reload: true);
            _btnLoadSel.Click += (s, e) => DoLoad(LoadScope.Selected, reload: false);
            _btnAll.Click     += (s, e) => SetAllChecked(true);
            _btnNone.Click    += (s, e) => SetAllChecked(false);
            _btnReport.Click  += (s, e) => OpenReport();

            _tooltip.SetToolTip(_btnScan,    "Re-scan disk  (F5)");
            _tooltip.SetToolTip(_btnLoad,    "Load checked files — skip already-OK  (F2)");
            _tooltip.SetToolTip(_btnReload,  "Force-reload ALL checked files");
            _tooltip.SetToolTip(_btnLoadSel, "Load only highlighted grid row(s)  (F3)");
            _tooltip.SetToolTip(_btnAll,     "Check all visible rows  (Ctrl+A)");
            _tooltip.SetToolTip(_btnNone,    "Uncheck all visible rows");
            _tooltip.SetToolTip(_btnReport,  "Open full load-session report");

            Controls.Add(tb);
        }

        // ── Right-click context menu ──────────────────────────────────────────

        private void BuildContextMenu()
        {
            _ctxMenu = new ContextMenuStrip {
                BackColor  = Color.FromArgb(36, 36, 48),
                ForeColor  = ColText,
                Font       = new Font("Segoe UI", 9f),
                RenderMode = ToolStripRenderMode.System,
            };

            CtxItem("▶  Load Selected",          OnCtxLoad,       "Load selected rows on ACAD thread");
            CtxItem("🔄  Reload Selected",        OnCtxReload,     "Force-reload selected rows");
            _ctxMenu.Items.Add(new ToolStripSeparator());
            CtxItem("📂  Open Folder",            OnCtxOpenFolder, "Open file's folder in Explorer");
            CtxItem("📝  Open in Editor",         OnCtxOpenEditor, "Open in Notepad++ or Notepad");
            CtxItem("📋  Copy Path",              OnCtxCopyPath,   "Copy full path to clipboard");
            CtxItem("📋  Copy All Paths",         OnCtxCopyAllPaths,"Copy paths of all selected rows");
            _ctxMenu.Items.Add(new ToolStripSeparator());
            CtxItem("☑  Check Selected",          OnCtxCheck,      "Tick selected rows");
            CtxItem("☐  Uncheck Selected",        OnCtxUncheck,    "Untick selected rows");
        }

        private void CtxItem(string text, EventHandler h, string tip)
        {
            var item = new ToolStripMenuItem(text) {
                BackColor = Color.FromArgb(36, 36, 48),
                ForeColor = ColText,
            };
            item.Click += h;
            _ctxMenu.Items.Add(item);
        }

        // ── Status bar + progress ─────────────────────────────────────────────

        private void BuildStatusBar()
        {
            var bar = new Panel {
                Dock      = DockStyle.Bottom,
                Height    = 28,
                BackColor = Color.FromArgb(18, 18, 26),
            };

            _lblStatus = new Label {
                Dock      = DockStyle.Fill,
                ForeColor = ColSub,
                Font      = new Font("Consolas", 8f),
                TextAlign = ContentAlignment.MiddleLeft,
                Padding   = new Padding(10, 0, 0, 0),
            };

            _progress = new ProgressBar {
                Dock    = DockStyle.Right,
                Width   = 180,
                Minimum = 0,
                Maximum = 100,
                Value   = 0,
                Style   = ProgressBarStyle.Continuous,
                Visible = false,
            };

            bar.Controls.Add(_lblStatus);
            bar.Controls.Add(_progress);
            Controls.Add(bar);
        }

        // ── Split area: grid (top) / log (bottom) ─────────────────────────────

        private void BuildSplitArea()
        {
            _split = new SplitContainer {
                Dock             = DockStyle.Fill,
                Orientation      = Orientation.Horizontal,
                Panel1MinSize    = 180,
                Panel2MinSize    = 60,
                SplitterDistance = 510,
                BackColor        = BgDark,
                SplitterWidth    = 5,
            };
            BuildGrid();
            BuildLogPanel();
            Controls.Add(_split);
        }

        // ── Grid ──────────────────────────────────────────────────────────────

        private void BuildGrid()
        {
            _grid = new DataGridView {
                Dock              = DockStyle.Fill,
                BackgroundColor   = BgGrid,
                BorderStyle       = BorderStyle.None,
                RowHeadersVisible = false,
                AllowUserToAddRows    = false,
                AllowUserToDeleteRows = false,
                ReadOnly          = false,
                SelectionMode     = DataGridViewSelectionMode.FullRowSelect,
                MultiSelect       = true,
                AutoSizeRowsMode  = DataGridViewAutoSizeRowsMode.None,
                ColumnHeadersHeightSizeMode =
                    DataGridViewColumnHeadersHeightSizeMode.DisableResizing,
                ColumnHeadersHeight = 28,
                RowTemplate         = { Height = 22 },
                GridColor           = Color.FromArgb(40, 40, 54),
                DefaultCellStyle    = {
                    BackColor          = BgGrid,
                    ForeColor          = ColText,
                    SelectionBackColor = Color.FromArgb(48, 88, 138),
                    SelectionForeColor = Color.White,
                    Font               = new Font("Consolas", 8.5f),
                    Padding            = new Padding(3, 0, 3, 0),
                },
                ColumnHeadersDefaultCellStyle = {
                    BackColor          = Color.FromArgb(42, 42, 56),
                    ForeColor          = ColSub,
                    Font               = new Font("Segoe UI", 8.5f, FontStyle.Bold),
                    SelectionBackColor = Color.FromArgb(42, 42, 56),
                },
                EnableHeadersVisualStyles = false,
                ClipboardCopyMode         = DataGridViewClipboardCopyMode.EnableWithoutHeaderText,
                ContextMenuStrip          = _ctxMenu,
            };

            // Checkbox column
            _grid.Columns.Add(new DataGridViewCheckBoxColumn {
                HeaderText   = "☑",
                Width        = 30,
                ReadOnly     = false,
                Resizable    = DataGridViewTriState.False,
                AutoSizeMode = DataGridViewAutoSizeColumnMode.None,
            });

            AddCol("Status",    58,  false, "center", "Load result: OK / FAIL / MISSING / SKIP");
            AddCol("#",         46,  false, "center", "Row number after sort");
            AddCol("Cat",       68,  false, "center", "AutoCAD or Civil 3D");
            AddCol("Menu",      155, false, "left",   "Parent _menu folder name");
            AddCol("File",      230, true,  "left",   "File name — DLLs show version stamp after load");
            AddCol("Ext",       42,  false, "center", "File extension");
            AddCol("ms",        50,  false, "center", "Load time in milliseconds");
            AddCol("Full path", 380, true,  "left",   "Absolute path on disk");

            _grid.CellFormatting   += GridCellFormatting;
            _grid.CellValueChanged += GridCellValueChanged;
            _grid.CellDoubleClick  += GridCellDoubleClick;
            _grid.CellMouseMove    += GridCellMouseMove;

            // Commit checkbox immediately
            _grid.CurrentCellDirtyStateChanged += (s, e) => {
                if (_grid.IsCurrentCellDirty)
                    _grid.CommitEdit(DataGridViewDataErrorContexts.Commit);
            };

            // Header checkbox col → toggle all
            _grid.ColumnHeaderMouseClick += (s, e) => {
                if (e.ColumnIndex == CI_Chk)
                    SetAllChecked(_visible.Any(f => !f.Checked));
            };

            // Right-click — ensure clicked row is selected first
            _grid.CellMouseDown += (s, e) => {
                if (e.Button == MouseButtons.Right && e.RowIndex >= 0)
                    if (!_grid.Rows[e.RowIndex].Selected)
                    {
                        _grid.ClearSelection();
                        _grid.Rows[e.RowIndex].Selected = true;
                    }
            };

            _split.Panel1.Controls.Add(_grid);
        }

        // ── Log panel ─────────────────────────────────────────────────────────

        private void BuildLogPanel()
        {
            var logBar = new Panel { Dock = DockStyle.Top, Height = 32, BackColor = BgPanel };

            logBar.Controls.Add(new Label {
                Text      = "📋 Live Log",
                Location  = new Point(8, 7),
                Size      = new Size(82, 20),
                ForeColor = ColSub,
                Font      = new Font("Segoe UI", 8.5f, FontStyle.Bold),
                BackColor = Color.Transparent,
            });

            _btnStoreLog = LogBtn("💾 Store Log", Color.FromArgb(30, 60, 40),
                                  Color.FromArgb(55, 120, 75), 94);
            _btnStoreLog.Click += OnStoreLog;
            _tooltip.SetToolTip(_btnStoreLog,
                "Save live log to a timestamped file in the ss\\ folder");

            _btnClearLog = LogBtn("🗑 Clear", Color.FromArgb(50, 30, 30),
                                  Color.FromArgb(110, 55, 55), 192);
            _btnClearLog.Size   = new Size(68, 24);
            _btnClearLog.Click += (s, e) => _logBox.Clear();

            _chkAutoScroll = new CheckBox {
                Text      = "Auto-scroll",
                Location  = new Point(268, 7),
                Size      = new Size(92, 20),
                Checked   = true,
                ForeColor = ColSub,
                Font      = new Font("Segoe UI", 8f),
                BackColor = Color.Transparent,
            };

            logBar.Controls.AddRange(new Control[] {
                _btnStoreLog, _btnClearLog, _chkAutoScroll });

            _logBox = new RichTextBox {
                Dock        = DockStyle.Fill,
                BackColor   = BgLog,
                ForeColor   = ColText,
                Font        = new Font("Consolas", 8f),
                ReadOnly    = true,
                ScrollBars  = RichTextBoxScrollBars.Both,
                WordWrap    = false,
                BorderStyle = BorderStyle.None,
            };

            _split.Panel2.Controls.Add(_logBox);
            _split.Panel2.Controls.Add(logBar);
        }

        private Button LogBtn(string text, Color bg, Color border, int x)
        {
            var b = new Button {
                Text      = text,
                Location  = new Point(x, 4),
                Size      = new Size(90, 24),
                FlatStyle = FlatStyle.Flat,
                BackColor = bg,
                ForeColor = ColText,
                Font      = new Font("Segoe UI", 8f),
                Cursor    = Cursors.Hand,
                FlatAppearance = { BorderColor = border, BorderSize = 1 },
            };
            return b;
        }

        // ═════════════════════════════════════════════════════════════════════
        // FILE SYSTEM WATCHER — auto-rescan when disk changes
        // ═════════════════════════════════════════════════════════════════════

        private void SetupWatcher()
        {
            try
            {
                if (!Directory.Exists(DiskScanner.BaseFolder)) return;

                _watcher = new FileSystemWatcher(DiskScanner.BaseFolder) {
                    IncludeSubdirectories = true,
                    NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite |
                                   NotifyFilters.DirectoryName,
                    Filter  = "*.*",
                    EnableRaisingEvents = true,
                };

                // Debounce: wait 3 s of quiet after the last event before rescanning
                _rescanTimer = new System.Windows.Forms.Timer { Interval = 3000 };
                _rescanTimer.Tick += (s, e) => {
                    _rescanTimer.Stop();
                    if (IsHandleCreated && !IsDisposed)
                        Invoke((MethodInvoker)(() => {
                            LogManager.Info("LoaderWindow",
                                "FileSystemWatcher: disk change detected — rescanning.");
                            DoScan();
                        }));
                };

                FileSystemEventHandler kick = (s, e) => {
                    string ext = System.IO.Path.GetExtension(e.Name ?? "")
                                       .ToLowerInvariant();
                    if (ext == ".lsp" || ext == ".vlx" || ext == ".fas" || ext == ".dll")
                        if (IsHandleCreated && !IsDisposed)
                            Invoke((MethodInvoker)(() => _rescanTimer.Start()));
                };

                _watcher.Created += kick;
                _watcher.Deleted += kick;
                _watcher.Renamed += (s, e) => kick(s,
                    new FileSystemEventArgs(WatcherChangeTypes.Changed, e.FullPath, e.Name));

                LogManager.Info("LoaderWindow",
                    "FileSystemWatcher active on: " + DiskScanner.BaseFolder);
            }
            catch (System.Exception ex)
            {
                LogManager.Warn("LoaderWindow",
                    "FileSystemWatcher setup failed: " + ex.Message);
            }
        }

        private void TearDownWatcher()
        {
            try
            {
                _rescanTimer?.Stop();
                _rescanTimer?.Dispose();
                _watcher?.Dispose();
            }
            catch { }
        }

        // ═════════════════════════════════════════════════════════════════════
        // CHECKED-STATE PERSISTENCE
        // ═════════════════════════════════════════════════════════════════════
        // Simple line-delimited file: one full path per line = file is checked.
        // (Keeps net48 compat without pulling in System.Text.Json / Newtonsoft.)

        private void SaveCheckedState()
        {
            try
            {
                Directory.CreateDirectory(LogManager.LogFolder);
                var lines = _allFiles
                    .Where(f => f.Checked)
                    .Select(f => f.FullPath);
                File.WriteAllLines(CheckedFile, lines);
            }
            catch (System.Exception ex)
            {
                LogManager.Warn("LoaderWindow",
                    "Could not save checked state: " + ex.Message);
            }
        }

        private void LoadCheckedState()
        {
            try
            {
                if (!File.Exists(CheckedFile)) return;
                var checked_ = new System.Collections.Generic.HashSet<string>(
                    File.ReadAllLines(CheckedFile),
                    StringComparer.OrdinalIgnoreCase);

                foreach (var f in _allFiles)
                    f.Checked = checked_.Contains(f.FullPath);

                // Refresh grid checkboxes
                for (int i = 0; i < _visible.Count && i < _grid.Rows.Count; i++)
                    _grid.Rows[i].Cells[CI_Chk].Value = _visible[i].Checked;
            }
            catch (System.Exception ex)
            {
                LogManager.Warn("LoaderWindow",
                    "Could not restore checked state: " + ex.Message);
            }
        }

        // ═════════════════════════════════════════════════════════════════════
        // LIVE LOG
        // ═════════════════════════════════════════════════════════════════════

        private void OnLogLine(string line)
        {
            if (IsDisposed) return;
            if (InvokeRequired)
            {
                try { Invoke(new Action<string>(OnLogLine), line); }
                catch { }
                return;
            }
            AppendLogLine(line);
        }

        private void AppendLogLine(string line)
        {
            if (_logBox == null || _logBox.IsDisposed) return;

            Color c = ColText;
            if (line.IndexOf("[ERROR]", StringComparison.OrdinalIgnoreCase) >= 0) c = ColError;
            else if (line.IndexOf("[WARN ]", StringComparison.OrdinalIgnoreCase) >= 0) c = ColWarn;
            else if (line.IndexOf("[INFO ]", StringComparison.OrdinalIgnoreCase) >= 0) c = ColInfo;
            else if (line.StartsWith("───") || line.StartsWith("---"))              c = ColSub;

            int start = _logBox.TextLength;
            _logBox.AppendText(line + "\n");
            _logBox.Select(start, line.Length);
            _logBox.SelectionColor  = c;
            _logBox.SelectionLength = 0;

            if (_chkAutoScroll.Checked)
                _logBox.ScrollToCaret();
        }

        private void OnStoreLog(object? sender, EventArgs e)
        {
            try
            {
                string stamp   = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss");
                string outFile = System.IO.Path.Combine(LogManager.LogFolder,
                                     "LispLoaderExtra_stored_" + stamp + ".log");
                Directory.CreateDirectory(LogManager.LogFolder);
                File.WriteAllText(outFile, _logBox.Text);
                LogManager.Info("LoaderWindow", "Log stored → " + outFile);
                MessageBox.Show("Log saved to:\n" + outFile, "LispLoaderExtra",
                    MessageBoxButtons.OK, MessageBoxIcon.Information);
            }
            catch (System.Exception ex)
            {
                MessageBox.Show("Failed to store log:\n" + ex.Message, "LispLoaderExtra",
                    MessageBoxButtons.OK, MessageBoxIcon.Warning);
            }
        }

        // ═════════════════════════════════════════════════════════════════════
        // CONTEXT MENU HANDLERS
        // ═════════════════════════════════════════════════════════════════════

        private void OnCtxLoad(object? s, EventArgs e)    => DoLoad(LoadScope.Selected, false);
        private void OnCtxReload(object? s, EventArgs e)  => DoLoad(LoadScope.Selected, true);
        private void OnCtxCheck(object? s, EventArgs e)   => SetSelectedChecked(true);
        private void OnCtxUncheck(object? s, EventArgs e) => SetSelectedChecked(false);

        private void OnCtxOpenFolder(object? s, EventArgs e)
        {
            var f = GetFirstSelectedFile();
            if (f == null) return;
            try
            {
                if (File.Exists(f.FullPath))
                    Process.Start("explorer.exe", "/select,\"" + f.FullPath + "\"");
                else
                {
                    string? dir = System.IO.Path.GetDirectoryName(f.FullPath);
                    if (dir != null && Directory.Exists(dir))
                        Process.Start("explorer.exe", dir);
                }
            }
            catch (System.Exception ex)
            {
                LogManager.Warn("LoaderWindow", "Explorer open failed: " + ex.Message);
            }
        }

        private void OnCtxOpenEditor(object? s, EventArgs e)
        {
            var f = GetFirstSelectedFile();
            if (f == null) return;
            if (!File.Exists(f.FullPath))
            {
                SetStatus("File not found: " + f.FullPath);
                return;
            }
            try
            {
                // Try Notepad++ first (most common on dev machines)
                string npp = System.IO.Path.Combine(
                    Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles),
                    "Notepad++", "notepad++.exe");
                if (!File.Exists(npp))
                    npp = System.IO.Path.Combine(
                        Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86),
                        "Notepad++", "notepad++.exe");

                string editor = File.Exists(npp) ? npp : "notepad.exe";
                Process.Start(editor, "\"" + f.FullPath + "\"");
            }
            catch (System.Exception ex)
            {
                LogManager.Warn("LoaderWindow", "Open in editor failed: " + ex.Message);
            }
        }

        private void OnCtxCopyPath(object? s, EventArgs e)
        {
            var f = GetFirstSelectedFile();
            if (f == null) return;
            try { Clipboard.SetText(f.FullPath); }
            catch { }
        }

        private void OnCtxCopyAllPaths(object? s, EventArgs e)
        {
            var paths = GetSelectedFiles().Select(f => f.FullPath);
            try { Clipboard.SetText(string.Join(Environment.NewLine, paths)); }
            catch { }
        }

        // ── Selection helpers ─────────────────────────────────────────────────

        private ScannedFile? GetFirstSelectedFile()
        {
            foreach (DataGridViewRow row in _grid.SelectedRows)
                if (row.Tag is ScannedFile f) return f;
            return null;
        }

        private List<ScannedFile> GetSelectedFiles()
        {
            var list = new List<ScannedFile>();
            var rows = _grid.SelectedRows.Cast<DataGridViewRow>()
                            .OrderBy(r => r.Index);
            foreach (var row in rows)
                if (row.Tag is ScannedFile f) list.Add(f);
            return list;
        }

        private void SetSelectedChecked(bool value)
        {
            foreach (DataGridViewRow row in _grid.SelectedRows)
            {
                row.Cells[CI_Chk].Value = value;
                if (row.Tag is ScannedFile f) f.Checked = value;
            }
            _grid.Refresh();
        }

        // ═════════════════════════════════════════════════════════════════════
        // SCAN
        // ═════════════════════════════════════════════════════════════════════

        private void DoScan()
        {
            if (_scanning) return;
            _scanning        = true;
            _btnScan.Text    = "Scanning…";
            _btnScan.Enabled = false;
            SetStatus("Scanning disk…");
            Application.DoEvents();

            _grid.Rows.Clear();
            _allFiles.Clear();

            try
            {
                _allFiles = DiskScanner.Scan();
                LoadCheckedState();        // restore ticks after rescan
                RefreshNameDropdown();
                UpdateTitleAndStatus();
            }
            catch (System.Exception ex)
            {
                LogManager.Error("LoaderWindow", "Scan failed: " + ex.Message);
                SetStatus("Scan failed: " + ex.Message);
            }
            finally
            {
                _scanning        = false;
                _btnScan.Text    = "🔍 Scan";
                _btnScan.Enabled = true;
            }
        }

        // ═════════════════════════════════════════════════════════════════════
        // DROPDOWNS / FILTER
        // ═════════════════════════════════════════════════════════════════════

        private void OnCatChanged(object sender, EventArgs e)  => RefreshNameDropdown();
        private void OnNameChanged(object sender, EventArgs e) => ApplyFilters();

        private void RefreshNameDropdown()
        {
            string cat = _cbCat.SelectedItem?.ToString() ?? "All";
            _cbName.SelectedIndexChanged -= OnNameChanged;
            _cbName.Items.Clear();
            _cbName.Items.Add("All");
            foreach (var n in DiskScanner.GetMenuNames(cat, _allFiles))
                _cbName.Items.Add(n);
            _cbName.SelectedIndex = 0;
            _cbName.SelectedIndexChanged += OnNameChanged;
            ApplyFilters();
        }

        private void ApplyFilters()
        {
            string cat    = _cbCat.SelectedItem?.ToString()  ?? "All";
            string name   = _cbName.SelectedItem?.ToString() ?? "All";
            string search = _txtSearch.Text.Trim();

            _visible = _allFiles
                .Where(f => cat  == "All" ||
                            string.Equals(f.Category, cat,  StringComparison.OrdinalIgnoreCase))
                .Where(f => name == "All" ||
                            string.Equals(f.MenuName, name, StringComparison.OrdinalIgnoreCase))
                .Where(f => search == "" ||
                            f.FileName.IndexOf(search, StringComparison.OrdinalIgnoreCase) >= 0 ||
                            f.MenuName.IndexOf(search, StringComparison.OrdinalIgnoreCase) >= 0)
                .ToList();

            PopulateGrid();
            UpdateTitleAndStatus();
        }

        // ═════════════════════════════════════════════════════════════════════
        // GRID POPULATION
        // ═════════════════════════════════════════════════════════════════════

        private void PopulateGrid()
        {
            _grid.SuspendLayout();
            _grid.Rows.Clear();

            foreach (var f in _visible)
            {
                int i   = _grid.Rows.Add();
                var row = _grid.Rows[i];
                row.Tag = f;

                row.Cells[CI_Chk   ].Value = f.Checked;
                row.Cells[CI_Status].Value = StatusLabel(f);
                row.Cells[CI_Num   ].Value = f.RowIndex.ToString();
                row.Cells[CI_Cat   ].Value = f.Category;
                row.Cells[CI_Menu  ].Value = f.MenuName;
                row.Cells[CI_File  ].Value = FileDisplayName(f);
                row.Cells[CI_Ext   ].Value = f.TypeLabel;
                row.Cells[CI_Ms    ].Value = f.LastResult != null
                    ? f.LastResult.Duration.TotalMilliseconds.ToString("0") : "";
                row.Cells[CI_Path  ].Value = f.FullPath;
            }

            _grid.ResumeLayout();
        }

        private void RefreshRow(int gridRow, ScannedFile f)
        {
            if (gridRow < 0 || gridRow >= _grid.Rows.Count) return;
            var row = _grid.Rows[gridRow];
            row.Cells[CI_Status].Value = StatusLabel(f);
            row.Cells[CI_File  ].Value = FileDisplayName(f);
            row.Cells[CI_Ms    ].Value = f.LastResult != null
                ? f.LastResult.Duration.TotalMilliseconds.ToString("0") : "";
        }

        private static string FileDisplayName(ScannedFile f)
        {
            if (f.Extension == ".dll" &&
                f.LastResult?.Status == LoadStatus.OK &&
                !string.IsNullOrEmpty(f.LastResult.ErrorMessage))
                return f.FileName + "  ·  " + f.LastResult.ErrorMessage;
            return f.FileName;
        }

        private static string StatusLabel(ScannedFile f) =>
            f.LastResult == null ? "" :
            f.LastResult.Status == LoadStatus.OK       ? "OK"      :
            f.LastResult.Status == LoadStatus.Failed   ? "FAIL"    :
            f.LastResult.Status == LoadStatus.NotFound ? "MISSING" :
            f.LastResult.Status == LoadStatus.Skipped  ? "SKIP"    : "";

        // ═════════════════════════════════════════════════════════════════════
        // LOAD / RELOAD
        // ═════════════════════════════════════════════════════════════════════

        private enum LoadScope { Checked, Selected }

        private void DoLoad(LoadScope scope, bool reload)
        {
            if (_loading) { SetStatus("Load already in progress."); return; }

            // ── Build work list ───────────────────────────────────────────────
            var toLoad = new List<(ScannedFile file, int row)>();

            if (scope == LoadScope.Selected)
            {
                var rows = _grid.SelectedRows.Cast<DataGridViewRow>()
                                .OrderBy(r => r.Index);
                foreach (var row in rows)
                {
                    if (!(row.Tag is ScannedFile f)) continue;
                    int vi = _visible.IndexOf(f);
                    if (vi >= 0) toLoad.Add((f, vi));
                }
            }
            else
            {
                for (int i = 0; i < _visible.Count; i++)
                {
                    var f    = _visible[i];
                    bool chk = _grid.Rows[i].Cells[CI_Chk].Value as bool? ?? false;
                    if (!chk) continue;
                    if (!reload && f.LastResult?.Status == LoadStatus.OK) continue;
                    toLoad.Add((f, i));
                }
            }

            if (toLoad.Count == 0)
            {
                SetStatus(scope == LoadScope.Selected
                    ? "No rows selected."
                    : reload ? "Nothing checked to reload."
                             : "All checked files already OK — use 🔄 Reload to force.");
                return;
            }

            // ── Lock UI ───────────────────────────────────────────────────────
            _loading            = true;
            _btnLoad.Enabled    = false;
            _btnReload.Enabled  = false;
            _btnLoadSel.Enabled = false;
            _btnScan.Enabled    = false;
            _progress.Maximum   = toLoad.Count;
            _progress.Value     = 0;
            _progress.Visible   = true;
            _flashPaths.Clear();

            try
            {
                LoadSession.Begin();
                int ok = 0, fail = 0, miss = 0;

                for (int idx = 0; idx < toLoad.Count; idx++)
                {
                    var (file, gridRow) = toLoad[idx];
                    LoadStatus prevStatus = file.LastResult?.Status ?? LoadStatus.Skipped;

                    SetStatus("Loading " + (idx + 1) + " / " + toLoad.Count +
                              ":  " + file.FileName);
                    _progress.Value = idx + 1;
                    Application.DoEvents();

                    // FileLoader.Load marshals to ACAD thread internally
                    LoadResult result = FileLoader.Load(file.FullPath);
                    file.LastResult   = result;
                    LoadSession.Add(result);
                    RefreshRow(gridRow, file);
                    _grid.InvalidateRow(gridRow);
                    Application.DoEvents();

                    // Track status changes for flash highlighting
                    if (result.Status != prevStatus)
                        _flashPaths.Add(file.FullPath);

                    switch (result.Status)
                    {
                        case LoadStatus.OK:       ok++;   break;
                        case LoadStatus.Failed:   fail++; break;
                        case LoadStatus.NotFound: miss++; break;
                    }
                }

                _grid.Refresh();
                FlashChangedRows();     // brief highlight for rows that changed status

                string summary = "Done — ✓ OK: " + ok;
                if (fail > 0) summary += "   ✗ FAILED: " + fail;
                if (miss > 0) summary += "   ⚠ NOT FOUND: " + miss;
                summary += "   (" + LoadSession.TotalDuration.TotalSeconds.ToString("0.0") + "s)";
                if (fail > 0 || miss > 0) summary += "   — 📊 Report for details";
                SetStatus(summary);
                UpdateTitleAndStatus();

                LogManager.Separator();
                LogManager.Info("LoaderWindow", LoadSession.SummaryLine);
                LogManager.Separator();
            }
            finally
            {
                _loading            = false;
                _btnLoad.Enabled    = true;
                _btnReload.Enabled  = true;
                _btnLoadSel.Enabled = true;
                _btnScan.Enabled    = true;
                _progress.Visible   = false;
            }
        }

        // ── Status-change flash ───────────────────────────────────────────────
        // Briefly highlights rows whose status changed so the user can spot
        // what actually changed without scanning the whole grid.

        private void FlashChangedRows()
        {
            if (_flashPaths.Count == 0) return;

            // Paint flash colour for 600 ms then restore normal formatting
            var flashTimer = new System.Windows.Forms.Timer { Interval = 600 };
            flashTimer.Tick += (s, e) => {
                flashTimer.Stop();
                flashTimer.Dispose();
                _flashPaths.Clear();
                _grid.Refresh();
            };

            // The CellFormatting handler already checks _flashPaths
            _grid.Refresh();
            flashTimer.Start();
        }

        // ═════════════════════════════════════════════════════════════════════
        // GRID EVENTS
        // ═════════════════════════════════════════════════════════════════════

        private void GridCellValueChanged(object sender, DataGridViewCellEventArgs e)
        {
            if (e.ColumnIndex != CI_Chk || e.RowIndex < 0) return;
            if (_grid.Rows[e.RowIndex].Tag is ScannedFile f)
                f.Checked = _grid.Rows[e.RowIndex].Cells[CI_Chk].Value as bool? ?? false;
        }

        private void GridCellDoubleClick(object sender, DataGridViewCellEventArgs e)
        {
            if (e.RowIndex < 0 || e.ColumnIndex == CI_Chk) return;
            if (!(_grid.Rows[e.RowIndex].Tag is ScannedFile f)) return;
            try
            {
                if (File.Exists(f.FullPath))
                    Process.Start("explorer.exe", "/select,\"" + f.FullPath + "\"");
                else
                {
                    string? dir = System.IO.Path.GetDirectoryName(f.FullPath);
                    if (dir != null && Directory.Exists(dir))
                        Process.Start("explorer.exe", dir);
                }
            }
            catch (System.Exception ex)
            {
                LogManager.Warn("LoaderWindow", "Explorer open failed: " + ex.Message);
            }
        }

        private string _lastTooltipPath = "";

        private void GridCellMouseMove(object sender, DataGridViewCellMouseEventArgs e)
        {
            if (e.RowIndex < 0) return;
            if (!(_grid.Rows[e.RowIndex].Tag is ScannedFile f)) return;
            if (f.FullPath == _lastTooltipPath) return;
            _lastTooltipPath = f.FullPath;

            string tip = f.FullPath;
            if (f.LastResult?.Status == LoadStatus.Failed)
                tip += "\n\nError: " + f.LastResult.ErrorMessage;
            else if (f.LastResult?.Status == LoadStatus.OK && f.Extension == ".dll")
                tip += "\n\nLoaded OK  (" +
                       f.LastResult.Duration.TotalMilliseconds.ToString("0") + " ms)" +
                       "\nVersion: " + f.LastResult.ErrorMessage;
            else if (f.LastResult?.Status == LoadStatus.OK)
                tip += "\n\nLoaded OK  (" +
                       f.LastResult.Duration.TotalMilliseconds.ToString("0") + " ms)";

            tip += "\n\nRight-click for options  •  Double-click to open folder";
            _tooltip.SetToolTip(_grid, tip);
        }

        private void GridCellFormatting(object sender, DataGridViewCellFormattingEventArgs e)
        {
            if (e.RowIndex < 0 || e.RowIndex >= _grid.Rows.Count) return;
            if (!(_grid.Rows[e.RowIndex].Tag is ScannedFile f)) return;

            // Flash rows that changed status in the last load
            if (_flashPaths.Contains(f.FullPath))
            {
                e.CellStyle.BackColor = Color.FromArgb(60, 55, 20);
                e.CellStyle.ForeColor = ColFlash;
                return;
            }

            // Status column
            if (e.ColumnIndex == CI_Status && f.LastResult != null)
            {
                e.CellStyle.ForeColor = f.LastResult.Status == LoadStatus.OK   ? ColOk  :
                                        f.LastResult.Status == LoadStatus.Failed ? ColFail :
                                        f.LastResult.Status == LoadStatus.NotFound ? ColMiss :
                                        ColSub;
                e.CellStyle.Font = new Font("Consolas", 8f, FontStyle.Bold);
            }

            // Extension / file colour
            if (e.ColumnIndex == CI_Ext || e.ColumnIndex == CI_File)
            {
                e.CellStyle.ForeColor = f.Extension == ".lsp" ? ColLsp :
                                        f.Extension == ".vlx" ? ColVlx :
                                        f.Extension == ".fas" ? ColFas :
                                        f.Extension == ".dll" ? ColDll : ColText;

                // DLL with version: italic
                if (e.ColumnIndex == CI_File && f.Extension == ".dll" &&
                    f.LastResult?.Status == LoadStatus.OK)
                    e.CellStyle.Font = new Font("Consolas", 8.5f, FontStyle.Italic);
            }

            // Category colour
            if (e.ColumnIndex == CI_Cat)
                e.CellStyle.ForeColor = f.Category == "Civil 3D" ? ColC3D : ColAcad;

            // Dim missing rows
            if (f.LastResult?.Status == LoadStatus.NotFound)
                e.CellStyle.ForeColor = Color.FromArgb(
                    (int)(e.CellStyle.ForeColor.R * 0.5),
                    (int)(e.CellStyle.ForeColor.G * 0.5),
                    (int)(e.CellStyle.ForeColor.B * 0.5));
        }

        // ═════════════════════════════════════════════════════════════════════
        // KEYBOARD SHORTCUTS
        // ═════════════════════════════════════════════════════════════════════

        private void OnKeyDown(object sender, KeyEventArgs e)
        {
            switch (e.KeyCode)
            {
                case Keys.F5:
                    DoScan();
                    e.Handled = true;
                    break;
                case Keys.F2:
                    DoLoad(LoadScope.Checked, false);
                    e.Handled = true;
                    break;
                case Keys.F3:
                    DoLoad(LoadScope.Selected, false);
                    e.Handled = true;
                    break;
                case Keys.A when e.Control:
                    SetAllChecked(true);
                    e.Handled = true;
                    break;
            }
        }

        // ═════════════════════════════════════════════════════════════════════
        // HELPERS
        // ═════════════════════════════════════════════════════════════════════

        private void SetAllChecked(bool value)
        {
            _grid.SuspendLayout();
            foreach (DataGridViewRow row in _grid.Rows)
            {
                row.Cells[CI_Chk].Value = value;
                if (row.Tag is ScannedFile f) f.Checked = value;
            }
            _grid.ResumeLayout();
            _grid.Refresh();
        }

        private void OpenReport()
        {
            using var win = new ReportWindow();
            win.ShowDialog(this);
        }

        private void SetStatus(string text) => _lblStatus.Text = text;

        private void UpdateTitleAndStatus()
        {
            int total  = _visible.Count;
            int lsp    = _visible.Count(f => f.Extension == ".lsp");
            int vlx    = _visible.Count(f => f.Extension == ".vlx");
            int fas    = _visible.Count(f => f.Extension == ".fas");
            int dll    = _visible.Count(f => f.Extension == ".dll");
            int loaded = _visible.Count(f => f.LastResult?.Status == LoadStatus.OK);
            int failed = _visible.Count(f => f.LastResult?.Status == LoadStatus.Failed);

            _lblTitle.Text =
                "LispLoaderExtra   " + _allFiles.Count + " files discovered" +
                (_allFiles.Count != total ? "  │  " + total + " showing" : "");

            string s = "Showing " + total +
                       "  │  LSP: " + lsp + "  VLX: " + vlx +
                       "  FAS: " + fas + "  DLL: " + dll;
            if (loaded > 0 || failed > 0)
                s += "  │  ✓ " + loaded + "  ✗ " + failed;
            SetStatus(s);
        }

        // ── Widget factories ──────────────────────────────────────────────────

        private void AddCol(string hdr, int w, bool fill, string align, string tip = "")
        {
            _grid.Columns.Add(new DataGridViewTextBoxColumn {
                HeaderText   = hdr,
                Width        = w,
                ReadOnly     = true,
                AutoSizeMode = fill
                    ? DataGridViewAutoSizeColumnMode.Fill
                    : DataGridViewAutoSizeColumnMode.None,
                SortMode     = DataGridViewColumnSortMode.Automatic,
                Resizable    = DataGridViewTriState.True,
                ToolTipText  = tip,
                DefaultCellStyle = {
                    Alignment = align == "center"
                        ? DataGridViewContentAlignment.MiddleCenter
                        : DataGridViewContentAlignment.MiddleLeft,
                },
            });
        }

        private static Label Lbl(string text, int x, int y, int w) =>
            new Label {
                Text      = text,
                Location  = new Point(x, y),
                Size      = new Size(w, 20),
                ForeColor = ColSub,
                BackColor = Color.Transparent,
                Font      = new Font("Segoe UI", 9f),
                TextAlign = ContentAlignment.MiddleLeft,
            };

        private static ComboBox Combo(int x, int y, int w) =>
            new ComboBox {
                Location      = new Point(x, y),
                Size          = new Size(w, 26),
                DropDownStyle = ComboBoxStyle.DropDownList,
                BackColor     = Color.FromArgb(48, 48, 62),
                ForeColor     = ColText,
                Font          = new Font("Segoe UI", 9f),
                FlatStyle     = FlatStyle.Flat,
            };

        private static Button Btn(string text, Color border, int w, int x, Panel tb)
        {
            var b = new Button {
                Text      = text,
                Location  = new Point(x, 6),
                Size      = new Size(w, 30),
                FlatStyle = FlatStyle.Flat,
                BackColor = Color.FromArgb(
                    Math.Max(0, border.R / 3),
                    Math.Max(0, border.G / 3),
                    Math.Max(0, border.B / 3)),
                ForeColor = Color.FromArgb(220, 220, 232),
                Font      = new Font("Segoe UI", 8.5f),
                Cursor    = Cursors.Hand,
                FlatAppearance = { BorderColor = border, BorderSize = 1 },
            };
            if (x > 0) tb.Controls.Add(b);
            return b;
        }
    }
}


// ===== FILE: ReportWindow.cs =====
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Windows.Forms;

namespace LispLoaderExtra
{
    /// <summary>
    /// WinForms report window — shows the full load session results.
    /// Open anytime by typing LISPLOADREPORT in AutoCAD.
    /// </summary>
    internal sealed class ReportWindow : Form
    {
        // ── Colours ───────────────────────────────────────────────────────────
        private static readonly Color BgDark    = Color.FromArgb(30,  30,  30);
        private static readonly Color BgPanel   = Color.FromArgb(40,  40,  40);
        private static readonly Color BgGrid    = Color.FromArgb(25,  25,  25);
        private static readonly Color ColOk     = Color.FromArgb(60,  180, 90);
        private static readonly Color ColFail   = Color.FromArgb(220, 60,  60);
        private static readonly Color ColMiss   = Color.FromArgb(220, 150, 40);
        private static readonly Color ColSkip   = Color.FromArgb(120, 120, 120);
        private static readonly Color ColText   = Color.FromArgb(220, 220, 220);
        private static readonly Color ColSub    = Color.FromArgb(150, 150, 150);
        private static readonly Color ColAccent = Color.FromArgb(80,  140, 210);

        // ── Controls ──────────────────────────────────────────────────────────
        private DataGridView _grid       = null!;
        private Label        _lblSummary = null!;
        private Label        _lblStats   = null!;
        private TextBox      _txtError   = null!;
        private Button       _btnAll     = null!;
        private Button       _btnErrors  = null!;
        private Button       _btnOk      = null!;
        private Button       _btnCopy    = null!;
        private Button       _btnLog     = null!;
        private Button       _btnReload  = null!;
        private Label        _lblVersion = null!;

        private List<LoadResult> _current = new List<LoadResult>();

        // ── Constructor ───────────────────────────────────────────────────────

        internal ReportWindow()
        {
            BuildLayout();
            Refresh_All();
        }

        // ── Layout ────────────────────────────────────────────────────────────

        private void BuildLayout()
        {
            Text            = "LispLoaderExtra — Load Report";
            Size            = new Size(1050, 680);
            MinimumSize     = new Size(800, 500);
            StartPosition   = FormStartPosition.CenterScreen;
            BackColor       = BgDark;
            ForeColor       = ColText;
            Font            = new Font("Segoe UI", 9f);
            FormBorderStyle = FormBorderStyle.Sizable;

            // ── Top bar ───────────────────────────────────────────────────────
            var top = new Panel {
                Dock = DockStyle.Top, Height = 90, BackColor = BgPanel,
                Padding = new Padding(12, 8, 12, 8)
            };

            _lblSummary = MakeLabel("", 13f, bold: true);
            _lblSummary.Location = new Point(12, 10);
            _lblSummary.Size     = new Size(700, 24);
            _lblSummary.ForeColor = ColAccent;

            _lblStats = MakeLabel("", 9f);
            _lblStats.Location = new Point(12, 36);
            _lblStats.Size     = new Size(900, 18);
            _lblStats.ForeColor = ColSub;

            _lblVersion = MakeLabel("", 8.5f);
            _lblVersion.Location  = new Point(12, 56);
            _lblVersion.Size      = new Size(900, 16);
            _lblVersion.ForeColor = ColSub;

            top.Controls.AddRange(new Control[] { _lblSummary, _lblStats, _lblVersion });

            // ── Toolbar ───────────────────────────────────────────────────────
            var toolbar = new Panel {
                Dock = DockStyle.Top, Height = 38, BackColor = BgPanel,
                Padding = new Padding(8, 4, 8, 4)
            };

            _btnAll    = MakeFilterBtn("All",     0);
            _btnErrors = MakeFilterBtn("Errors",  1);
            _btnOk     = MakeFilterBtn("OK only", 2);

            _btnCopy = MakeActionBtn("📋 Copy Report", ColAccent);
            _btnLog  = MakeActionBtn("📄 Open Log",    Color.FromArgb(70,70,90));
            _btnReload = MakeActionBtn("🔄 Reload",    Color.FromArgb(60,90,60));

            _btnCopy.Click   += (s,e) => CopyReport();
            _btnLog.Click    += (s,e) => OpenLog();
            _btnReload.Click += (s,e) => Refresh_All();
            _btnAll.Click    += (s,e) => ApplyFilter(null);
            _btnErrors.Click += (s,e) => ApplyFilter(LoadStatus.Failed);
            _btnOk.Click     += (s,e) => ApplyFilter(LoadStatus.OK);

            // Layout buttons left-to-right
            int x = 8;
            foreach (var b in new[] { _btnAll, _btnErrors, _btnOk })
            { b.Location = new Point(x, 5); x += b.Width + 4; }

            x += 20;
            foreach (var b in new[] { _btnCopy, _btnLog, _btnReload })
            { b.Location = new Point(x, 5); x += b.Width + 4; }

            toolbar.Controls.AddRange(new Control[] {
                _btnAll, _btnErrors, _btnOk, _btnCopy, _btnLog, _btnReload });

            // ── Grid ──────────────────────────────────────────────────────────
            _grid = new DataGridView {
                Dock              = DockStyle.Fill,
                BackgroundColor   = BgGrid,
                BorderStyle       = BorderStyle.None,
                RowHeadersVisible = false,
                AllowUserToAddRows    = false,
                AllowUserToDeleteRows = false,
                ReadOnly          = true,
                SelectionMode     = DataGridViewSelectionMode.FullRowSelect,
                MultiSelect       = false,
                AutoSizeRowsMode  = DataGridViewAutoSizeRowsMode.None,
                ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.DisableResizing,
                ColumnHeadersHeight = 28,
                RowTemplate         = { Height = 22 },
                GridColor           = Color.FromArgb(50, 50, 50),
                DefaultCellStyle    = {
                    BackColor    = BgGrid,
                    ForeColor    = ColText,
                    SelectionBackColor = Color.FromArgb(55, 95, 145),
                    SelectionForeColor = Color.White,
                    Font         = new Font("Consolas", 8.5f),
                    Padding      = new Padding(3, 0, 3, 0),
                },
                ColumnHeadersDefaultCellStyle = {
                    BackColor = Color.FromArgb(50, 50, 60),
                    ForeColor = ColSub,
                    Font      = new Font("Segoe UI", 8.5f, FontStyle.Bold),
                    SelectionBackColor = Color.FromArgb(50,50,60),
                },
                EnableHeadersVisualStyles = false,
            };

            AddCol("Status",    60,  false);
            AddCol("Type",      42,  false);
            AddCol("File name", 220, true);
            AddCol("ms",        48,  false);
            AddCol("Full path", 480, true);

            _grid.SelectionChanged += GridSelectionChanged;
            _grid.CellFormatting   += GridCellFormatting;

            // ── Error detail panel ────────────────────────────────────────────
            var errPanel = new Panel {
                Dock      = DockStyle.Bottom,
                Height    = 90,
                BackColor = Color.FromArgb(35, 20, 20),
                Padding   = new Padding(8, 4, 8, 4),
            };

            var errLabel = MakeLabel("Error detail:", 8.5f, bold: true);
            errLabel.ForeColor = ColFail;
            errLabel.Location  = new Point(8, 4);
            errLabel.Size      = new Size(100, 16);

            _txtError = new TextBox {
                Multiline   = true,
                ReadOnly    = true,
                ScrollBars  = ScrollBars.Vertical,
                BackColor   = Color.FromArgb(35, 20, 20),
                ForeColor   = Color.FromArgb(240, 100, 100),
                Font        = new Font("Consolas", 8.5f),
                BorderStyle = BorderStyle.None,
                Location    = new Point(8, 22),
                Size        = new Size(1020, 60),
                Anchor      = AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Top | AnchorStyles.Bottom,
                Text        = "(select a row to see error detail)",
            };
            errPanel.Controls.AddRange(new Control[] { errLabel, _txtError });

            // ── Splitter + content area ────────────────────────────────────────
            var split = new SplitContainer {
                Dock        = DockStyle.Fill,
                Orientation = Orientation.Horizontal,
                Panel1MinSize = 200,
                Panel2MinSize = 60,
                SplitterDistance = 430,
                BackColor   = BgDark,
            };
            split.Panel1.Controls.Add(_grid);
            split.Panel2.Controls.Add(_txtError);
            split.Panel2.Controls.Add(errLabel);

            Controls.Add(split);
            Controls.Add(toolbar);
            Controls.Add(top);
        }

        // ── Data ─────────────────────────────────────────────────────────────

        private void Refresh_All()
        {
            _current = LoadSession.Results.ToList();
            PopulateGrid(_current);
            UpdateSummary();
        }

        private void ApplyFilter(LoadStatus? status)
        {
            _current = status.HasValue
                ? LoadSession.Results.Where(r => r.Status == status.Value).ToList()
                : LoadSession.Results.ToList();
            PopulateGrid(_current);

            // Highlight active filter button
            foreach (var b in new[] { _btnAll, _btnErrors, _btnOk })
                b.FlatAppearance.BorderColor = BgPanel;

            var active = status switch {
                LoadStatus.Failed => _btnErrors,
                LoadStatus.OK     => _btnOk,
                _                 => _btnAll,
            };
            active.FlatAppearance.BorderColor = ColAccent;
        }

        private void PopulateGrid(List<LoadResult> items)
        {
            _grid.Rows.Clear();
            foreach (var r in items)
            {
                int i = _grid.Rows.Add(
                    StatusLabel(r.Status),
                    r.TypeLabel,
                    r.FileName,
                    r.Duration.TotalMilliseconds.ToString("0"),
                    r.Path);
                _grid.Rows[i].Tag = r;
            }
        }

        private void UpdateSummary()
        {
            int total = LoadSession.TotalCount;
            int ok    = LoadSession.OkCount;
            int fail  = LoadSession.FailCount;
            int miss  = LoadSession.NotFoundCount;

            _lblSummary.Text =
                $"Loaded {ok} / {total} files   " +
                (fail > 0 ? $"⚠  {fail} error(s)   " : "") +
                (miss > 0 ? $"⚠  {miss} not found   " : "");

            _lblStats.Text =
                $"LSP: {LoadSession.CountByType(".lsp")}   " +
                $"VLX: {LoadSession.CountByType(".vlx")}   " +
                $"FAS: {LoadSession.CountByType(".fas")}   " +
                $"DLL: {LoadSession.CountByType(".dll")}   " +
                $"│  Total time: {LoadSession.TotalDuration.TotalSeconds:0.00}s   " +
                $"│  Log: {LogManager.LogFolder}";

            _lblVersion.Text =
                $"Config: {ConfigReader.ConfigFile}";
        }

        // ── Grid events ───────────────────────────────────────────────────────

        private void GridSelectionChanged(object? sender, EventArgs e)
        {
            if (_grid.SelectedRows.Count == 0) { _txtError.Text = ""; return; }
            var r = _grid.SelectedRows[0].Tag as LoadResult;
            if (r == null) { _txtError.Text = ""; return; }

            _txtError.Text = r.Status switch {
                LoadStatus.OK       => $"OK  —  {r.Path}\r\nLoaded in {r.Duration.TotalMilliseconds:0} ms",
                LoadStatus.Failed   => $"FAILED  —  {r.Path}\r\n\r\n{r.ErrorMessage}",
                LoadStatus.NotFound => $"NOT FOUND  —  {r.Path}\r\n\r\nCheck that the path in LoaderConfig.txt is correct.",
                LoadStatus.Skipped  => $"SKIPPED  —  {r.Path}\r\n\r\n{r.ErrorMessage}",
                _                   => "",
            };
        }

        private void GridCellFormatting(object? sender, DataGridViewCellFormattingEventArgs e)
        {
            if (e.RowIndex < 0 || e.RowIndex >= _grid.Rows.Count) return;
            var r = _grid.Rows[e.RowIndex].Tag as LoadResult;
            if (r == null) return;

            Color fg = r.Status switch {
                LoadStatus.OK       => ColOk,
                LoadStatus.Failed   => ColFail,
                LoadStatus.NotFound => ColMiss,
                LoadStatus.Skipped  => ColSkip,
                _                   => ColText,
            };

            // Colour the Status column strongly, dim the rest slightly
            if (e.ColumnIndex == 0)
                e.CellStyle.ForeColor = fg;
            else if (r.Status != LoadStatus.OK)
                e.CellStyle.ForeColor = Color.FromArgb(
                    (fg.R + ColText.R) / 2,
                    (fg.G + ColText.G) / 2,
                    (fg.B + ColText.B) / 2);
        }

        // ── Actions ───────────────────────────────────────────────────────────

        private void CopyReport()
        {
            try
            {
                Clipboard.SetText(LoadSession.BuildTextReport());
                MessageBox.Show("Report copied to clipboard.", "LispLoaderExtra",
                    MessageBoxButtons.OK, MessageBoxIcon.Information);
            }
            catch { }
        }

        private void OpenLog()
        {
            string log = Path.Combine(LogManager.LogFolder, "LispLoaderExtra.log");
            if (File.Exists(log))
                Process.Start("notepad.exe", log);
            else
                MessageBox.Show($"Log file not found:\n{log}", "LispLoaderExtra",
                    MessageBoxButtons.OK, MessageBoxIcon.Warning);
        }

        // ── Helpers ───────────────────────────────────────────────────────────

        private void AddCol(string header, int width, bool fill)
        {
            var c = new DataGridViewTextBoxColumn {
                HeaderText  = header,
                Width       = width,
                AutoSizeMode = fill
                    ? DataGridViewAutoSizeColumnMode.Fill
                    : DataGridViewAutoSizeColumnMode.None,
                SortMode    = DataGridViewColumnSortMode.Automatic,
                Resizable   = DataGridViewTriState.True,
            };
            _grid.Columns.Add(c);
        }

        private static Label MakeLabel(string text, float size, bool bold = false) =>
            new Label {
                Text      = text,
                Font      = new Font("Segoe UI", size,
                                bold ? FontStyle.Bold : FontStyle.Regular),
                ForeColor = ColText,
                BackColor = Color.Transparent,
                AutoSize  = false,
            };

        private static Button MakeFilterBtn(string text, int _) =>
            new Button {
                Text      = text,
                Size      = new Size(72, 28),
                FlatStyle = FlatStyle.Flat,
                BackColor = Color.FromArgb(50, 50, 60),
                ForeColor = ColText,
                Font      = new Font("Segoe UI", 8.5f),
                Cursor    = Cursors.Hand,
                FlatAppearance = { BorderColor = Color.FromArgb(70,70,80), BorderSize = 1 },
            };

        private static Button MakeActionBtn(string text, Color bg) =>
            new Button {
                Text      = text,
                Size      = new Size(110, 28),
                FlatStyle = FlatStyle.Flat,
                BackColor = Color.FromArgb(bg.R / 2, bg.G / 2, bg.B / 2),
                ForeColor = ColText,
                Font      = new Font("Segoe UI", 8.5f),
                Cursor    = Cursors.Hand,
                FlatAppearance = { BorderColor = bg, BorderSize = 1 },
            };

        private static string StatusLabel(LoadStatus s) => s switch {
            LoadStatus.OK       => "OK",
            LoadStatus.Failed   => "FAIL",
            LoadStatus.NotFound => "MISSING",
            LoadStatus.Skipped  => "SKIP",
            _                   => "?",
        };
    }
}


// ===== FILE: ScannedFile.cs =====
using System;

namespace LispLoaderExtra
{
    /// <summary>
    /// One file discovered by the disk scanner.
    /// This is the pre-load data model (before any load attempt).
    /// </summary>
    internal sealed class ScannedFile
    {
        internal int    RowIndex    { get; set; }       // display #
        internal string Category   { get; }             // "AutoCAD" | "Civil 3D"
        internal string MenuName   { get; }             // e.g. "3D_Menu", "Alignment_menu"
        internal string FileName   { get; }             // e.g. "QuickMirror.lsp"
        internal string Extension  { get; }             // ".lsp" | ".vlx" | ".fas" | ".dll"
        internal string FullPath   { get; }
        internal bool   Checked    { get; set; } = true;

        // Set after a load attempt
        internal LoadResult? LastResult { get; set; }

        internal string TypeLabel =>
            Extension.TrimStart('.').ToUpperInvariant();

        internal ScannedFile(
            string category, string menuName,
            string fileName, string extension, string fullPath)
        {
            Category  = category;
            MenuName  = menuName;
            FileName  = fileName;
            Extension = extension.ToLowerInvariant();
            FullPath  = fullPath;
        }
    }
}


// ===== FILE: LispLoaderExtra.cs =====
using System;
using System.IO;
using Autodesk.AutoCAD.Runtime;

[assembly: ExtensionApplication(typeof(LispLoaderExtra.LispLoaderExtraApp))]

namespace LispLoaderExtra
{
    /// <summary>
    /// Plugin entry point — loaded once via _.netload.
    ///
    /// Initialize() does nothing except:
    ///   1. Print the version banner to the AutoCAD command line + log
    ///   2. Ensure the log folder exists
    ///   3. Tell the user which commands are available
    ///
    /// NO files are auto-loaded. The user opens the window and decides.
    ///
    /// Commands registered by ReportCommand.cs:
    ///   LISPLOADEXTRA   — opens the loader window (scan + checkbox + load)
    ///   LISPLOADREPORT  — opens the report window (last session results)
    /// </summary>
    public class LispLoaderExtraApp : IExtensionApplication
    {
        private const string Source = "LispLoaderExtra";

        public void Initialize()
        {
            try
            {
                // Ensure log folder exists so LogManager can write immediately
                Directory.CreateDirectory(LogManager.LogFolder);
            }
            catch { }

            // Version banner
            VersionStamp.Print();

            // Commands hint
            LogManager.Info(Source, "Ready.  Type:");
            LogManager.Info(Source, "   LISPLOADEXTRA   → open loader window");
            LogManager.Info(Source, "   LISPLOADREPORT  → open last-session report");
            LogManager.Separator();
        }

        public void Terminate()
        {
            LogManager.Info(Source, "LispLoaderExtra unloaded.");
        }
    }
}


// ===== FILE: LogManager.cs =====
using System;
using System.Collections.Generic;
using System.IO;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.EditorInput;

namespace LispLoaderExtra
{
    internal enum LogLevel { INFO, WARN, ERROR }

    /// <summary>
    /// Writes timestamped log lines to:
    ///   1. The AutoCAD command line
    ///   2. The persistent .log file on disk
    ///   3. An in-memory ring-buffer (last 2000 lines) for the live log panel
    ///
    /// Format:  HH:mm:ss  [LEVEL]  [Source]  message
    /// Example: 05:29:43  [INFO ]  [PvSplitApi]  SplitDataLines.Add(…)
    ///
    /// Subscribers (e.g. LoaderWindow log panel) register via LogManager.Subscribe().
    /// Log file rotates to .bak.log at 5 MB.
    /// </summary>
    internal static class LogManager
    {
        // ── Paths ─────────────────────────────────────────────────────────────
        internal static readonly string LogFolder =
            @"C:\Users\mateid\OneDrive - DB E.C.O. Group\@C3D\@Dan_Templates\Acad\Support_files\Lisps\Help_File\Lisp_menu\LispLoaderExtra\ss";

        private static readonly string LogFile =
            Path.Combine(LogFolder, "LispLoaderExtra.log");

        private const long MaxLogBytes = 5 * 1024 * 1024;   // 5 MB

        // ── In-memory ring-buffer (for live log panel) ─────────────────────
        private const int RingSize = 2000;
        private static readonly Queue<string> _ring = new Queue<string>(RingSize);
        private static readonly object _ringLock = new object();

        // ── Subscriber callbacks ───────────────────────────────────────────
        // The log panel in LoaderWindow subscribes here.
        private static event Action<string>? _lineAdded;

        internal static void Subscribe(Action<string> handler)
        {
            lock (_ringLock)
                _lineAdded += handler;
        }

        internal static void Unsubscribe(Action<string> handler)
        {
            lock (_ringLock)
                _lineAdded -= handler;
        }

        /// <summary>Returns a snapshot of the in-memory log (oldest first).</summary>
        internal static string[] GetRingSnapshot()
        {
            lock (_ringLock)
                return _ring.ToArray();
        }

        // ── Public API ────────────────────────────────────────────────────────
        internal static void Info (string source, string message) => Write(LogLevel.INFO,  source, message);
        internal static void Warn (string source, string message) => Write(LogLevel.WARN,  source, message);
        internal static void Error(string source, string message) => Write(LogLevel.ERROR, source, message);
        internal static void Separator() => Write(null, null, new string('─', 72));

        // ── Core ──────────────────────────────────────────────────────────────
        private static void Write(LogLevel? level, string? source, string message)
        {
            string line = Format(level, source, message);

            // 1. AutoCAD command line
            try
            {
                Document? doc = Application.DocumentManager?.MdiActiveDocument;
                doc?.Editor?.WriteMessage("\n" + line);
            }
            catch { }

            // 2. Disk — ensure folder exists first
            try
            {
                Directory.CreateDirectory(LogFolder);
                RotateIfNeeded();
                File.AppendAllText(LogFile, line + Environment.NewLine);
            }
            catch { }

            // 3. In-memory ring + subscriber notification
            Action<string>? notify = null;
            lock (_ringLock)
            {
                if (_ring.Count >= RingSize) _ring.Dequeue();
                _ring.Enqueue(line);
                notify = _lineAdded;
            }

            // Fire subscribers outside the lock so they can call back into LogManager
            try { notify?.Invoke(line); }
            catch { }
        }

        internal static string Format(LogLevel? level, string? source, string message)
        {
            string ts  = DateTime.Now.ToString("HH:mm:ss");
            string lvl = level.HasValue ? $"[{level.Value,-5}]" : "       ";
            string src = source != null ? $"[{source}]" : string.Empty;
            return $"{ts}  {lvl}  {src,-22}  {message}";
        }

        private static void RotateIfNeeded()
        {
            if (!File.Exists(LogFile)) return;
            if (new FileInfo(LogFile).Length < MaxLogBytes) return;
            string bak = Path.ChangeExtension(LogFile, ".bak.log");
            if (File.Exists(bak)) File.Delete(bak);
            File.Move(LogFile, bak);
        }
    }
}


// ===== FILE: ReportCommand.cs =====
using System;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Windows.Forms;
using Autodesk.AutoCAD.Runtime;

namespace LispLoaderExtra
{
    /// <summary>
    /// AutoCAD commands:
    ///   LISPLOADEXTRA   — open the loader window (modeless: stays open while you work)
    ///   LISPLOADREPORT  — open the report window (last session)
    ///   LISPLOADDIAG    — dump acmgd/accoremgd type names to log (debug)
    /// </summary>
    public static class ReportCommand
    {
        // Keep a single modeless instance alive so AutoCAD can continue working
        // while the form is open.  ShowDialog() blocks the ACAD command thread;
        // Show() does not — but it requires a message pump, which the Session
        // CommandFlags flag provides.
        private static LoaderWindow? _loaderInstance;

        [CommandMethod("LISPLOADEXTRA", CommandFlags.Session)]
        public static void ShowLoader()
        {
            System.Windows.Forms.Application.EnableVisualStyles();

            // If already open, bring it to front instead of opening a second copy
            if (_loaderInstance != null && !_loaderInstance.IsDisposed)
            {
                _loaderInstance.BringToFront();
                _loaderInstance.Activate();
                return;
            }

            _loaderInstance = new LoaderWindow();
            // Show() is modeless — AutoCAD keeps its message pump running,
            // so the form stays responsive and files load immediately.
            // We pass IntPtr.Zero so no parent window is set; the form floats
            // above AutoCAD via TopMost=true on its own.
            _loaderInstance.Show(new Win32Window(
                Autodesk.AutoCAD.ApplicationServices.Application.MainWindow.Handle));
        }

        [CommandMethod("LISPLOADREPORT", CommandFlags.Session)]
        public static void ShowReport()
        {
            System.Windows.Forms.Application.EnableVisualStyles();
            using var win = new ReportWindow();
            win.ShowDialog();
        }

        /// <summary>
        /// Diagnostic — type LISPLOADDIAG once after installing on a new
        /// AutoCAD version.
        /// </summary>
        [CommandMethod("LISPLOADDIAG", CommandFlags.Session)]
        public static void Diagnose()
        {
            const string Src = "LISPLOADDIAG";
            LogManager.Separator();
            LogManager.Info(Src, "Scanning ALL loaded assemblies for LISP-related methods...");

            int found = 0;
            string[] keywords = { "acedInvoke", "acedEval", "RunLisp", "LispInvoke", "EvalLisp" };

            foreach (Assembly asm in AppDomain.CurrentDomain.GetAssemblies())
            {
                string asmName = asm.GetName().Name ?? "";

                Type[] types;
                try { types = asm.GetTypes(); }
                catch { continue; }

                foreach (Type t in types)
                {
                    MethodInfo[] methods;
                    try
                    {
                        methods = t.GetMethods(
                            BindingFlags.Public | BindingFlags.NonPublic |
                            BindingFlags.Static | BindingFlags.Instance);
                    }
                    catch { continue; }

                    foreach (var m in methods)
                    {
                        bool match = false;
                        foreach (string kw in keywords)
                            if (m.Name.IndexOf(kw, StringComparison.OrdinalIgnoreCase) >= 0)
                            { match = true; break; }
                        if (!match) continue;

                        string sig = string.Join(", ",
                            System.Linq.Enumerable.Select(
                                m.GetParameters(),
                                p => p.ParameterType.Name + " " + p.Name));
                        LogManager.Info(Src,
                            $"[{asmName}] {t.FullName}.{m.Name}({sig})");
                        found++;
                    }
                }
            }

            try
            {
                using var rb = new Autodesk.AutoCAD.DatabaseServices.ResultBuffer(
                    new Autodesk.AutoCAD.DatabaseServices.TypedValue(5016),
                    new Autodesk.AutoCAD.DatabaseServices.TypedValue(5017));
                IntPtr ptr = rb.UnmanagedObject;
                LogManager.Info(Src, $"ResultBuffer.UnmanagedObject OK: 0x{ptr:X}");
            }
            catch (System.Exception ex)
            {
                LogManager.Warn(Src, "ResultBuffer.UnmanagedObject failed: " + ex.Message);
            }

            LogManager.Separator();
            LogManager.Info(Src, $"Total LISP methods found: {found}");
            LogManager.Separator();

            MessageBox.Show(
                $"Found {found} LISP-related method(s).\n" +
                $"Check log:\n{System.IO.Path.Combine(LogManager.LogFolder, "LispLoaderExtra.log")}",
                "LISPLOADDIAG",
                MessageBoxButtons.OK,
                found > 0 ? MessageBoxIcon.Information : MessageBoxIcon.Warning);
        }
    }

    /// <summary>
    /// Wraps an HWND so it can be passed to Form.Show(IWin32Window).
    /// This makes the WinForms form a child of the AutoCAD main window
    /// in the Windows Z-order, so it stays above AutoCAD without being
    /// on top of every other application.
    /// </summary>
    internal sealed class Win32Window : IWin32Window
    {
        public Win32Window(IntPtr handle) { Handle = handle; }
        public IntPtr Handle { get; }
    }
}

Additional info:

Share this page: