using OpenNest.Bending;
using OpenNest.CNC;
using OpenNest.Controls;
using OpenNest.Converters;
using OpenNest.Geometry;
using OpenNest.IO;
using OpenNest.IO.Bending;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace OpenNest.Forms
{
    public partial class CadConverterForm : Form
    {

        private SimplifierViewerForm simplifierViewer;
        private bool staleProgram = true;

        protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
        {
            if ((keyData == Keys.Up || keyData == Keys.Down) && !fileList.Focused)
            {
                fileList.ProcessArrowKey(keyData);
                return true;
            }

            return base.ProcessCmdKey(ref msg, keyData);
        }

        public CadConverterForm()
        {
            InitializeComponent();

            fileList.SelectedIndexChanged += OnFileSelected;
            filterPanel.FilterChanged += OnFilterChanged;
            filterPanel.BendLineSelected += OnBendLineSelected;
            filterPanel.BendLineRemoved += OnBendLineRemoved;
            filterPanel.BendLineEdited += OnBendLineEdited;
            filterPanel.AddBendLineClicked += OnAddBendLineClicked;
            entityView1.LinePicked += OnLinePicked;
            entityView1.PickCancelled += OnPickCancelled;
            entityView1.TextConvertRequested += OnTextConvertRequested;
            btnSplit.Click += OnSplitClicked;
            numQuantity.ValueChanged += OnQuantityChanged;
            txtCustomer.TextChanged += OnCustomerChanged;
            cboBendDetector.SelectedIndexChanged += OnBendDetectorChanged;

            // Populate bend detector dropdown
            cboBendDetector.Items.Add("Auto");
            foreach (var detector in BendDetectorRegistry.Detectors)
                cboBendDetector.Items.Add(detector.Name);
            cboBendDetector.SelectedIndex = 0;

            viewTabs.SelectedIndexChanged += OnViewTabChanged;

            // Drag & drop
            AllowDrop = true;
            DragEnter += OnDragEnter;
            DragDrop += OnDragDrop;
        }

        private FileListItem CurrentItem => fileList.SelectedItem;

        #region File Import

        public void AddFile(string file) => AddFile(file, 0, null);

        private void AddFile(string file, int detectorIndex, string detectorName)
        {
            try
            {
                var options = new CadImportOptions
                {
                    BendDetectorName = detectorIndex == 0 ? null : detectorName,
                };

                var result = CadImporter.Import(file, options);
                if (result.Entities.Count == 0)
                    return;

                var item = new FileListItem
                {
                    Name = result.Name,
                    Entities = result.Entities,
                    Path = result.SourcePath,
                    Quantity = 1,
                    Customer = string.Empty,
                    Bends = result.Bends,
                    Bounds = result.Bounds,
                    EntityCount = result.Entities.Count,
                    Texts = ExtractTexts(result.Document),
                };

                if (InvokeRequired)
                    BeginInvoke((Action)(() => fileList.AddItem(item)));
                else
                    fileList.AddItem(item);
            }
            catch (Exception ex)
            {
                MessageBox.Show($"Error importing \"{file}\": {ex.Message}");
            }
        }

        public void AddFiles(IEnumerable<string> files)
        {
            var fileArray = files.ToArray();
            // Capture UI state on main thread before entering parallel loop
            var detectorIndex = cboBendDetector.SelectedIndex;
            var detectorName = cboBendDetector.SelectedItem?.ToString();

            System.Threading.Tasks.Task.Run(() =>
            {
                Parallel.ForEach(fileArray, file => AddFile(file, detectorIndex, detectorName));
            });
        }

        #endregion

        #region Event Handlers

        private void OnFileSelected(object sender, int index)
        {
            var item = CurrentItem;
            if (item == null)
            {
                ClearDetailBar();
                return;
            }

            LoadItem(item);
            staleProgram = true;
            if (viewTabs.SelectedTab == tabProgram)
                LoadProgramTab();
            else
                programEditor.Clear();
        }

        private void LoadItem(FileListItem item)
        {
            entityView1.ClearPenCache();
            if (entityView1.IsPickingBendLine)
            {
                entityView1.IsPickingBendLine = false;
                filterPanel.SetPickMode(false);
            }
            entityView1.OriginalEntities = chkShowOriginal.Checked ? item.OriginalEntities : null;
            entityView1.Entities.Clear();
            entityView1.Entities.AddRange(item.Entities);
            entityView1.Bends = item.Bends ?? new List<Bend>();
            entityView1.Texts = item.Texts ?? new List<CadText>();

            item.Entities.ForEach(e => e.IsVisible = true);
            if (item.Entities.Any(e => e.Layer != null))
                item.Entities.ForEach(e => e.Layer.IsVisible = true);
            ReHidePromotedEntities(item.Bends);
            ReHideSuppressedEntities(item);

            filterPanel.LoadItem(item.Entities, item.Bends);

            numQuantity.Value = item.Quantity;
            txtCustomer.Text = item.Customer ?? "";

            var bounds = item.Bounds;
            lblDimensions.Text = bounds != null
                ? $"{bounds.Width:0.#} x {bounds.Length:0.#}"
                : "";
            lblEntityCount.Text = $"{item.EntityCount} entities";

            entityView1.ZoomToFit();
            CheckSimplifiable(item);
        }

        private void CheckSimplifiable(FileListItem item)
        {
            ResetSimplifyButton();

            // Only check original (unsimplified) entities
            var entities = item.OriginalEntities ?? item.Entities;
            if (entities == null || entities.Count < 10) return;

            // Quick line count check — need at least MinLines consecutive lines
            var lineCount = entities.Count(e => e is Geometry.Line);
            if (lineCount < 3) return;

            // Run a quick analysis on a background thread
            var capturedEntities = new List<Entity>(entities);
            Task.Run(() =>
            {
                var shapes = ShapeBuilder.GetShapes(capturedEntities);
                var simplifier = new GeometrySimplifier();
                var count = 0;
                foreach (var shape in shapes)
                    count += simplifier.Analyze(shape).Count;
                return count;
            }).ContinueWith(t =>
            {
                if (t.IsCompletedSuccessfully && t.Result > 0)
                    HighlightSimplifyButton(t.Result);
            }, TaskScheduler.FromCurrentSynchronizationContext());
        }

        private void HighlightSimplifyButton(int candidateCount)
        {
            btnSimplify.Text = $"Simplify ({candidateCount})";
            btnSimplify.BackColor = Color.FromArgb(60, 120, 60);
            btnSimplify.ForeColor = Color.White;
        }

        private void ResetSimplifyButton()
        {
            btnSimplify.Text = "Simplify...";
            btnSimplify.BackColor = SystemColors.Control;
            btnSimplify.ForeColor = SystemColors.ControlText;
        }

        private void ClearDetailBar()
        {
            numQuantity.Value = 1;
            txtCustomer.Text = "";
            lblDimensions.Text = "";
            lblEntityCount.Text = "";
            entityView1.Entities.Clear();
            entityView1.Invalidate();
        }

        private void OnFilterChanged(object sender, EventArgs e)
        {
            var item = CurrentItem;
            if (item == null) return;

            filterPanel.ApplyFilters(item.Entities);
            ReHidePromotedEntities(item.Bends);
            SyncSuppressedState(item);
            entityView1.Invalidate();
            staleProgram = true;
        }

        private void OnViewTabChanged(object sender, EventArgs e)
        {
            if (viewTabs.SelectedTab == tabProgram && staleProgram)
                LoadProgramTab();
        }

        private void LoadProgramTab()
        {
            var item = CurrentItem;
            if (item == null)
            {
                programEditor.Clear();
                staleProgram = false;
                return;
            }

            var entities = item.Entities.Where(en => en.Layer.IsVisible && en.IsVisible).ToList();
            if (entities.Count == 0)
            {
                programEditor.Clear();
                staleProgram = false;
                return;
            }

            var normalized = ShapeProfile.NormalizeEntities(entities);
            programEditor.LoadEntities(normalized);
            staleProgram = false;
        }

        private void OnBendLineSelected(object sender, int index)
        {
            entityView1.SelectedBendIndex = index;
            entityView1.Invalidate();
        }

        private void OnBendLineRemoved(object sender, int index)
        {
            var item = CurrentItem;
            if (item == null || index < 0 || index >= item.Bends.Count) return;

            var bend = item.Bends[index];
            if (bend.SourceEntity != null)
                bend.SourceEntity.IsVisible = true;

            item.Bends.RemoveAt(index);
            Bend.UpdateEtchEntities(item.Entities, item.Bends);
            entityView1.Entities.Clear();
            entityView1.Entities.AddRange(item.Entities);
            entityView1.Bends = item.Bends;
            entityView1.SelectedBendIndex = -1;
            filterPanel.LoadItem(item.Entities, item.Bends);
            entityView1.Invalidate();
        }

        private void OnBendLineEdited(object sender, int index)
        {
            var item = CurrentItem;
            if (item == null || index < 0 || index >= item.Bends.Count) return;

            var bend = item.Bends[index];
            using var dialog = new BendLineDialog();
            dialog.LoadBend(bend);

            if (dialog.ShowDialog(this) != DialogResult.OK) return;

            bend.Direction = dialog.Direction;
            bend.Angle = dialog.BendAngle;
            bend.Radius = dialog.BendRadius;

            Bend.UpdateEtchEntities(item.Entities, item.Bends);
            entityView1.Entities.Clear();
            entityView1.Entities.AddRange(item.Entities);
            entityView1.Bends = item.Bends;
            filterPanel.LoadItem(item.Entities, item.Bends);
            entityView1.Invalidate();
        }

        private void OnQuantityChanged(object sender, EventArgs e)
        {
            var item = CurrentItem;
            if (item == null) return;

            item.Quantity = (int)numQuantity.Value;
            fileList.Invalidate();
        }

        private void OnCustomerChanged(object sender, EventArgs e)
        {
            var item = CurrentItem;
            if (item != null)
                item.Customer = txtCustomer.Text;
        }

        private void OnBendDetectorChanged(object sender, EventArgs e)
        {
            // Re-run bend detection on current item if it has a document
            // For now, bend detection only runs at import time
        }

        private void OnSplitClicked(object sender, EventArgs e)
        {
            var item = CurrentItem;
            if (item == null) return;

            var entities = item.Entities.Where(en => en.Layer.IsVisible && en.IsVisible).ToList();
            if (entities.Count == 0) return;

            var normalized = ShapeProfile.NormalizeEntities(entities);
            var pgm = ConvertGeometry.ToProgram(normalized);
            var originOffset = Vector.Zero;
            if (pgm.Codes.Count > 0 && pgm[0].Type == CodeType.RapidMove)
            {
                var rapid = (RapidMove)pgm[0];
                originOffset = rapid.EndPoint;
                pgm.Offset(-originOffset);
            }

            var drawing = new Drawing(item.Name, pgm);
            drawing.Bends = item.Bends.Select(b => new Bend
            {
                StartPoint = new Vector(b.StartPoint.X - originOffset.X, b.StartPoint.Y - originOffset.Y),
                EndPoint = new Vector(b.EndPoint.X - originOffset.X, b.EndPoint.Y - originOffset.Y),
                Direction = b.Direction,
                Angle = b.Angle,
                Radius = b.Radius,
                NoteText = b.NoteText,
            }).ToList();

            using var form = new SplitDrawingForm(drawing);
            if (form.ShowDialog(this) != DialogResult.OK || form.ResultDrawings?.Count <= 1)
                return;

            // Write split DXF files and re-import
            var sourceDir = Path.GetDirectoryName(item.Path);
            var baseName = Path.GetFileNameWithoutExtension(item.Path);
            var writableDir = Directory.Exists(sourceDir) && IsDirectoryWritable(sourceDir)
                ? sourceDir
                : Path.GetTempPath();

            var index = fileList.SelectedIndex;

            var splitWriter = new SplitDxfWriter();
            var splitItems = new List<FileListItem>();

            for (var i = 0; i < form.ResultDrawings.Count; i++)
            {
                var splitDrawing = form.ResultDrawings[i];

                var splitName = $"{baseName}-{i + 1}.dxf";
                var splitPath = GetUniquePath(Path.Combine(writableDir, splitName));

                splitWriter.Write(splitPath, splitDrawing);

                // Re-import geometry but keep bends from the split drawing
                var result = Dxf.Import(splitPath);

                var splitItem = new FileListItem
                {
                    Name = Path.GetFileNameWithoutExtension(splitPath),
                    Entities = result.Entities,
                    Path = splitPath,
                    Quantity = item.Quantity,
                    Customer = item.Customer,
                    Bends = splitDrawing.Bends ?? new List<Bend>(),
                    Bounds = result.Entities.GetBoundingBox(),
                    EntityCount = result.Entities.Count
                };
                splitItems.Add(splitItem);
            }

            // Remove original and add split items directly (preserving bend info)
            fileList.RemoveAt(index);
            foreach (var splitItem in splitItems)
                fileList.AddItem(splitItem);

            if (writableDir != sourceDir)
                MessageBox.Show($"Split files written to: {writableDir}", "Split Output",
                    MessageBoxButtons.OK, MessageBoxIcon.Information);
        }

        private void OnAddBendLineClicked(object sender, EventArgs e)
        {
            var active = !entityView1.IsPickingBendLine;
            entityView1.IsPickingBendLine = active;
            filterPanel.SetPickMode(active);
        }

        private void OnLinePicked(object sender, Line line)
        {
            using var dialog = new BendLineDialog();
            if (dialog.ShowDialog(this) != DialogResult.OK)
                return;

            var item = CurrentItem;
            if (item == null) return;

            var bend = new Bend
            {
                StartPoint = line.StartPoint,
                EndPoint = line.EndPoint,
                Direction = dialog.Direction,
                Angle = dialog.BendAngle,
                Radius = dialog.BendRadius,
                SourceEntity = line
            };

            line.IsVisible = false;
            item.Bends.Add(bend);
            Bend.UpdateEtchEntities(item.Entities, item.Bends);
            entityView1.Entities.Clear();
            entityView1.Entities.AddRange(item.Entities);
            entityView1.Bends = item.Bends;
            filterPanel.LoadItem(item.Entities, item.Bends);
            entityView1.Invalidate();
        }

        private void OnPickCancelled(object sender, EventArgs e)
        {
            entityView1.IsPickingBendLine = false;
            filterPanel.SetPickMode(false);
        }

        private void OnTextConvertRequested(object sender, Controls.CadText text)
        {
            var item = CurrentItem;
            if (item == null) return;

            var font = LoadChrFont();
            if (font == null) return;

            var layer = new Geometry.Layer("ENGRAVE")
            {
                Color = System.Drawing.Color.Cyan,
                IsVisible = true,
            };

            var entities = font.RenderText(text.Value, text.Height, Geometry.Vector.Zero, layer);
            if (entities.Count > 0)
            {
                var box = entities.GetBoundingBox();
                var shiftX = text.HAlign switch
                {
                    System.Drawing.StringAlignment.Center => text.Position.X - (box.Left + box.Right) / 2,
                    System.Drawing.StringAlignment.Far => text.Position.X - box.Right,
                    _ => text.Position.X - box.Left,
                };
                var shiftY = text.VAlign switch
                {
                    System.Drawing.StringAlignment.Center => text.Position.Y - (box.Top + box.Bottom) / 2,
                    System.Drawing.StringAlignment.Near => text.Position.Y - box.Top,
                    _ => text.Position.Y - box.Bottom,
                };
                var shift = new Geometry.Vector(shiftX, shiftY);
                foreach (var e in entities)
                    e.Offset(shift);
            }
            if (entities.Count == 0)
            {
                MessageBox.Show($"No geometry produced for \"{text.Value}\".", "Convert Text",
                    MessageBoxButtons.OK, MessageBoxIcon.Information);
                return;
            }

            item.Entities.AddRange(entities);
            item.Texts.Remove(text);
            item.EntityCount = item.Entities.Count;
            item.Bounds = item.Entities.GetBoundingBox();

            entityView1.Entities.Clear();
            entityView1.Entities.AddRange(item.Entities);
            entityView1.Texts = item.Texts;
            filterPanel.LoadItem(item.Entities, item.Bends);
            entityView1.Invalidate();
            staleProgram = true;
            lblEntityCount.Text = $"{item.EntityCount} entities";
        }

        private ChrFont cachedChrFont;
        private string cachedChrFontPath;

        private ChrFont LoadChrFont()
        {
            if (cachedChrFont != null)
                return cachedChrFont;

            // Look for .CHR files next to the app, then prompt
            var appDir = System.IO.Path.GetDirectoryName(Application.ExecutablePath);
            var candidates = Directory.GetFiles(appDir, "*.CHR", SearchOption.TopDirectoryOnly);

            string fontPath;
            if (candidates.Length == 1)
            {
                fontPath = candidates[0];
            }
            else if (candidates.Length > 1)
            {
                fontPath = PromptForChrFile(appDir);
            }
            else
            {
                fontPath = PromptForChrFile(null);
            }

            if (fontPath == null)
                return null;

            try
            {
                cachedChrFont = ChrFont.Read(fontPath);
                cachedChrFontPath = fontPath;
                return cachedChrFont;
            }
            catch (Exception ex)
            {
                MessageBox.Show($"Error loading font: {ex.Message}", "Font Error",
                    MessageBoxButtons.OK, MessageBoxIcon.Error);
                return null;
            }
        }

        private static string PromptForChrFile(string initialDir)
        {
            using var dlg = new OpenFileDialog
            {
                Title = "Select Engraving Font (.CHR)",
                Filter = "Gravograph Font (*.CHR)|*.CHR",
                InitialDirectory = initialDir ?? "",
            };
            return dlg.ShowDialog() == DialogResult.OK ? dlg.FileName : null;
        }

        private void OnDragEnter(object sender, DragEventArgs e)
        {
            if (e.Data.GetDataPresent(DataFormats.FileDrop))
                e.Effect = DragDropEffects.Copy;
        }

        private void OnDragDrop(object sender, DragEventArgs e)
        {
            if (e.Data.GetDataPresent(DataFormats.FileDrop))
            {
                var files = (string[])e.Data.GetData(DataFormats.FileDrop);
                var dxfFiles = files.Where(f =>
                    f.EndsWith(".dxf", StringComparison.OrdinalIgnoreCase) ||
                    f.EndsWith(".dwg", StringComparison.OrdinalIgnoreCase)).ToArray();
                if (dxfFiles.Length > 0)
                    AddFiles(dxfFiles);
            }
        }

        private void OnSimplifyClick(object sender, EventArgs e)
        {
            if (entityView1.Entities == null || entityView1.Entities.Count == 0)
                return;

            // Always simplify from original geometry to prevent tolerance creep
            var item = CurrentItem;
            if (item != null && item.OriginalEntities == null)
                item.OriginalEntities = new List<Entity>(item.Entities);

            var sourceEntities = item?.OriginalEntities ?? entityView1.Entities;
            var shapes = ShapeBuilder.GetShapes(sourceEntities);
            if (shapes.Count == 0)
                return;

            if (simplifierViewer == null || simplifierViewer.IsDisposed)
            {
                simplifierViewer = new SimplifierViewerForm();
                simplifierViewer.Owner = this;
                simplifierViewer.Applied += OnSimplifierApplied;

                // Position next to this form
                var screen = Screen.FromControl(this);
                simplifierViewer.Location = new Point(
                    System.Math.Min(Right, screen.WorkingArea.Right - simplifierViewer.Width),
                    Top);
            }

            simplifierViewer.LoadShapes(shapes, entityView1);
        }

        private void OnSimplifierApplied(List<Entity> entities)
        {
            entityView1.Entities.Clear();
            entityView1.Entities.AddRange(entities);
            entityView1.ZoomToFit();
            entityView1.Invalidate();

            var item = CurrentItem;
            if (item != null)
            {
                item.Entities = entities;
                item.EntityCount = entities.Count;
                item.Bounds = entities.GetBoundingBox();
            }

            lblEntityCount.Text = $"{entities.Count} entities";
            ResetSimplifyButton();
        }

        private void OnShowOriginalChanged(object sender, EventArgs e)
        {
            var item = CurrentItem;
            entityView1.OriginalEntities = chkShowOriginal.Checked ? item?.OriginalEntities : null;
            entityView1.Invalidate();
        }

        private void OnLabelsChanged(object sender, EventArgs e)
        {
            entityView1.ShowEntityLabels = chkLabels.Checked;
            entityView1.Invalidate();
        }

        private void OnExportDxfClick(object sender, EventArgs e)
        {
            var item = CurrentItem;
            if (item == null) return;

            using var dlg = new SaveFileDialog
            {
                Filter = "DXF 2018 (*.dxf)|*.dxf|" +
                         "DXF 2013 (*.dxf)|*.dxf|" +
                         "DXF 2010 (*.dxf)|*.dxf|" +
                         "DXF 2007 (*.dxf)|*.dxf|" +
                         "DXF 2004 (*.dxf)|*.dxf|" +
                         "DXF 2000 (*.dxf)|*.dxf|" +
                         "DXF R14 (*.dxf)|*.dxf",
                FileName = Path.ChangeExtension(item.Name, ".dxf"),
            };

            if (dlg.ShowDialog() != DialogResult.OK) return;

            var version = dlg.FilterIndex switch
            {
                2 => ACadSharp.ACadVersion.AC1027,
                3 => ACadSharp.ACadVersion.AC1024,
                4 => ACadSharp.ACadVersion.AC1021,
                5 => ACadSharp.ACadVersion.AC1018,
                6 => ACadSharp.ACadVersion.AC1015,
                7 => ACadSharp.ACadVersion.AC1014,
                _ => ACadSharp.ACadVersion.AC1032,
            };

            var doc = new ACadSharp.CadDocument(version);
            foreach (var entity in item.Entities)
            {
                switch (entity)
                {
                    case Geometry.Line line:
                        doc.Entities.Add(new ACadSharp.Entities.Line
                        {
                            StartPoint = new CSMath.XYZ(line.StartPoint.X, line.StartPoint.Y, 0),
                            EndPoint = new CSMath.XYZ(line.EndPoint.X, line.EndPoint.Y, 0),
                        });
                        break;

                    case Geometry.Arc arc:
                        var startAngle = arc.StartAngle;
                        var endAngle = arc.EndAngle;
                        if (arc.IsReversed)
                            OpenNest.Math.Generic.Swap(ref startAngle, ref endAngle);
                        doc.Entities.Add(new ACadSharp.Entities.Arc
                        {
                            Center = new CSMath.XYZ(arc.Center.X, arc.Center.Y, 0),
                            Radius = arc.Radius,
                            StartAngle = startAngle,
                            EndAngle = endAngle,
                        });
                        break;

                    case Geometry.Circle circle:
                        doc.Entities.Add(new ACadSharp.Entities.Circle
                        {
                            Center = new CSMath.XYZ(circle.Center.X, circle.Center.Y, 0),
                            Radius = circle.Radius,
                        });
                        break;
                }
            }

            using var writer = new ACadSharp.IO.DxfWriter(dlg.FileName, doc, false);
            writer.Write();
        }

        #endregion

        #region Load Existing Drawings

        public void LoadDrawings(IEnumerable<Drawing> drawings)
        {
            foreach (var drawing in drawings)
            {
                List<Entity> entities;

                if (drawing.SourceEntities != null)
                {
                    // Use stored entities with stable GUIDs; apply suppression state
                    entities = new List<Entity>(drawing.SourceEntities);

                    foreach (var entity in entities)
                        entity.IsVisible = !drawing.SuppressedEntityIds.Contains(entity.Id);
                }
                else
                {
                    // Fallback: derive entities from Program (older drawings without source entities)
                    entities = ConvertProgram.ToGeometry(drawing.Program);

                    // Re-apply source offset so entities appear in their natural position
                    if (drawing.Source?.Offset != null && drawing.Source.Offset != Vector.Zero)
                    {
                        foreach (var entity in entities)
                            entity.Offset(drawing.Source.Offset);
                    }

                    // Remove rapid traversals — they aren't part of the cut geometry
                    entities.RemoveAll(e => e.Layer == SpecialLayers.Rapid);
                }

                var bounds = entities.GetBoundingBox();

                var item = new FileListItem
                {
                    Name = drawing.Name,
                    Entities = entities,
                    Path = drawing.Source?.Path,
                    Quantity = drawing.Quantity.Required,
                    Customer = drawing.Customer ?? string.Empty,
                    Bends = drawing.Bends?.ToList() ?? new List<Bend>(),
                    SuppressedEntityIds = drawing.SuppressedEntityIds.Count > 0
                        ? new HashSet<Guid>(drawing.SuppressedEntityIds)
                        : null,
                    Bounds = bounds,
                    EntityCount = entities.Count
                };

                fileList.AddItem(item);
            }
        }

        #endregion

        #region Output

        public List<Drawing> GetDrawings()
        {
            var drawings = new List<Drawing>();

            foreach (var item in fileList.Items)
            {
                var visible = item.Entities
                    .Where(e => e.Layer.IsVisible && e.IsVisible)
                    .ToList();

                if (visible.Count == 0)
                    continue;

                // Rebuild a CadImportResult from the FileListItem's current state so
                // BuildDrawing sees the user's edits (filters, suppressions, new bends).
                var result = new CadImportResult
                {
                    Entities = item.Entities,
                    Bends = item.Bends ?? new List<Bend>(),
                    Bounds = item.Bounds,
                    SourcePath = item.Path,
                    Name = item.Name,
                };

                var editedProgram = (item == CurrentItem && programEditor.IsDirty && programEditor.Program != null)
                    ? programEditor.Program
                    : null;

                var drawing = CadImporter.BuildDrawing(
                    result,
                    visible,
                    result.Bends,
                    item.Quantity,
                    item.Customer,
                    editedProgram);

                drawings.Add(drawing);

                Thread.Sleep(20);
            }

            return drawings;
        }

        #endregion

        #region Helpers

        private static void ReHidePromotedEntities(List<Bend> bends)
        {
            if (bends == null) return;
            foreach (var bend in bends)
            {
                if (bend.SourceEntity != null)
                    bend.SourceEntity.IsVisible = false;
            }
        }

        private static void ReHideSuppressedEntities(FileListItem item)
        {
            if (item.SuppressedEntityIds == null || item.SuppressedEntityIds.Count == 0)
                return;

            foreach (var entity in item.Entities)
            {
                if (item.SuppressedEntityIds.Contains(entity.Id))
                    entity.IsVisible = false;
            }

            // If all entities on a layer are suppressed, uncheck the layer too
            var layerGroups = item.Entities
                .Where(e => e.Layer != null)
                .GroupBy(e => e.Layer);

            foreach (var group in layerGroups)
            {
                if (group.All(e => !e.IsVisible))
                    group.Key.IsVisible = false;
            }
        }

        private static void SyncSuppressedState(FileListItem item)
        {
            var bendSources = new HashSet<Entity>(
                (item.Bends ?? new List<Bend>())
                    .Where(b => b.SourceEntity != null)
                    .Select(b => b.SourceEntity));

            var suppressed = item.Entities
                .Where(e => !(e.Layer.IsVisible && e.IsVisible))
                .Where(e => !bendSources.Contains(e))
                .Select(e => e.Id);

            item.SuppressedEntityIds = new HashSet<Guid>(suppressed);

            if (item.SuppressedEntityIds.Count == 0)
                item.SuppressedEntityIds = null;
        }

        private static bool IsDirectoryWritable(string path)
        {
            try
            {
                var testFile = Path.Combine(path, $".writetest_{Guid.NewGuid()}");
                File.WriteAllText(testFile, "");
                File.Delete(testFile);
                return true;
            }
            catch { return false; }
        }

        private static string GetUniquePath(string path)
        {
            if (!File.Exists(path)) return path;

            var dir = Path.GetDirectoryName(path);
            var name = Path.GetFileNameWithoutExtension(path);
            var ext = Path.GetExtension(path);
            var counter = 2;

            while (File.Exists(path))
            {
                path = Path.Combine(dir, $"{name}_{counter}{ext}");
                counter++;
            }

            return path;
        }

        #endregion

        private static List<CadText> ExtractTexts(ACadSharp.CadDocument doc)
        {
            var texts = new List<CadText>();
            if (doc == null) return texts;

            foreach (var entity in doc.Entities)
            {
                switch (entity)
                {
                    case ACadSharp.Entities.MText mtext:
                        var (mh, mv) = MapAttachmentPoint(mtext.AttachmentPoint);
                        texts.Add(new CadText
                        {
                            Position = new Vector(mtext.InsertPoint.X, mtext.InsertPoint.Y),
                            Value = ReplaceControlCodes(StripMTextFormatting(mtext.Value)),
                            Height = mtext.Height,
                            Rotation = mtext.Rotation,
                            LayerName = mtext.Layer?.Name,
                            HAlign = mh,
                            VAlign = mv,
                        });
                        break;

                    case ACadSharp.Entities.TextEntity text:
                        var useAlignment = text.HorizontalAlignment != 0
                            || text.VerticalAlignment != 0;
                        var pt = useAlignment ? text.AlignmentPoint : text.InsertPoint;
                        var ha = text.HorizontalAlignment switch
                        {
                            ACadSharp.Entities.TextHorizontalAlignment.Center => System.Drawing.StringAlignment.Center,
                            ACadSharp.Entities.TextHorizontalAlignment.Right => System.Drawing.StringAlignment.Far,
                            _ => System.Drawing.StringAlignment.Near,
                        };
                        var va = text.VerticalAlignment switch
                        {
                            ACadSharp.Entities.TextVerticalAlignmentType.Middle => System.Drawing.StringAlignment.Center,
                            ACadSharp.Entities.TextVerticalAlignmentType.Top => System.Drawing.StringAlignment.Near,
                            ACadSharp.Entities.TextVerticalAlignmentType.Bottom => System.Drawing.StringAlignment.Far,
                            _ => System.Drawing.StringAlignment.Far,
                        };
                        texts.Add(new CadText
                        {
                            Position = new Vector(pt.X, pt.Y),
                            Value = ReplaceControlCodes(text.Value),
                            Height = text.Height,
                            Rotation = text.Rotation,
                            LayerName = text.Layer?.Name,
                            HAlign = ha,
                            VAlign = va,
                        });
                        break;
                }
            }

            return texts;
        }

        private static (System.Drawing.StringAlignment h, System.Drawing.StringAlignment v) MapAttachmentPoint(
            ACadSharp.Entities.AttachmentPointType apt)
        {
            var h = apt switch
            {
                ACadSharp.Entities.AttachmentPointType.TopCenter
                    or ACadSharp.Entities.AttachmentPointType.MiddleCenter
                    or ACadSharp.Entities.AttachmentPointType.BottomCenter => System.Drawing.StringAlignment.Center,
                ACadSharp.Entities.AttachmentPointType.TopRight
                    or ACadSharp.Entities.AttachmentPointType.MiddleRight
                    or ACadSharp.Entities.AttachmentPointType.BottomRight => System.Drawing.StringAlignment.Far,
                _ => System.Drawing.StringAlignment.Near,
            };
            var v = apt switch
            {
                ACadSharp.Entities.AttachmentPointType.MiddleLeft
                    or ACadSharp.Entities.AttachmentPointType.MiddleCenter
                    or ACadSharp.Entities.AttachmentPointType.MiddleRight => System.Drawing.StringAlignment.Center,
                ACadSharp.Entities.AttachmentPointType.BottomLeft
                    or ACadSharp.Entities.AttachmentPointType.BottomCenter
                    or ACadSharp.Entities.AttachmentPointType.BottomRight => System.Drawing.StringAlignment.Far,
                _ => System.Drawing.StringAlignment.Near,
            };
            return (h, v);
        }

        private static string StripMTextFormatting(string text)
        {
            if (string.IsNullOrEmpty(text)) return text;
            var result = System.Text.RegularExpressions.Regex.Replace(text, @"\\[A-Za-z][^;]*;", "");
            result = result.Replace("{", "").Replace("}", "");
            return result.Trim();
        }

        private static string ReplaceControlCodes(string text)
        {
            if (string.IsNullOrEmpty(text)) return text;
            return text
                .Replace("%%p", "±")
                .Replace("%%P", "±")
                .Replace("%%d", "°")
                .Replace("%%D", "°")
                .Replace("%%c", "⌀")
                .Replace("%%C", "⌀")
                .Replace("%%%", "%");
        }

        private void filterPanel_Paint(object sender, PaintEventArgs e)
        {

        }
    }
}
