using OpenNest.Bending;
using OpenNest.CNC;
using OpenNest.Geometry;
using OpenNest.Math;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Linq;

namespace OpenNest.Controls
{
    internal class PlateRenderer
    {
        private readonly PlateView view;

        public PlateRenderer(PlateView view)
        {
            this.view = view;
        }

        public void DrawPlate(Graphics g)
        {
            var plate = view.Plate;
            var plateRect = new RectangleF
            {
                Width = view.LengthWorldToGui(plate.Size.Length),
                Height = view.LengthWorldToGui(plate.Size.Width)
            };

            var edgeSpacingRect = new RectangleF
            {
                Width = view.LengthWorldToGui(plate.Size.Length - plate.EdgeSpacing.Left - plate.EdgeSpacing.Right),
                Height = view.LengthWorldToGui(plate.Size.Width - plate.EdgeSpacing.Top - plate.EdgeSpacing.Bottom)
            };

            switch (plate.Quadrant)
            {
                case 1:
                    plateRect.Location = view.PointWorldToGraph(0, 0);
                    edgeSpacingRect.Location = view.PointWorldToGraph(
                        plate.EdgeSpacing.Left,
                        plate.EdgeSpacing.Bottom);
                    break;

                case 2:
                    plateRect.Location = view.PointWorldToGraph(-plate.Size.Length, 0);
                    edgeSpacingRect.Location = view.PointWorldToGraph(
                        plate.EdgeSpacing.Left - plate.Size.Length,
                        plate.EdgeSpacing.Bottom);
                    break;

                case 3:
                    plateRect.Location = view.PointWorldToGraph(-plate.Size.Length, -plate.Size.Width);
                    edgeSpacingRect.Location = view.PointWorldToGraph(
                        plate.EdgeSpacing.Left - plate.Size.Length,
                        plate.EdgeSpacing.Bottom - plate.Size.Width);
                    break;

                case 4:
                    plateRect.Location = view.PointWorldToGraph(0, -plate.Size.Width);
                    edgeSpacingRect.Location = view.PointWorldToGraph(
                        plate.EdgeSpacing.Left,
                        plate.EdgeSpacing.Bottom - plate.Size.Width);
                    break;

                default:
                    return;
            }

            plateRect.Y -= plateRect.Height;
            edgeSpacingRect.Y -= edgeSpacingRect.Height;

            g.FillRectangle(view.ColorScheme.LayoutFillBrush, plateRect);

            var viewBounds = view.GetViewBounds();

            if (!edgeSpacingRect.Contains(viewBounds))
            {
                g.DrawRectangle(view.ColorScheme.EdgeSpacingPen,
                   edgeSpacingRect.X,
                   edgeSpacingRect.Y,
                   edgeSpacingRect.Width,
                   edgeSpacingRect.Height);
            }

            g.DrawRectangle(view.ColorScheme.LayoutOutlinePen,
                plateRect.X,
                plateRect.Y,
                plateRect.Width,
                plateRect.Height);
        }

        public void DrawParts(Graphics g)
        {
            var viewBounds = view.GetViewBounds();
            var layoutParts = view.LayoutParts;

            for (var i = 0; i < layoutParts.Count; ++i)
            {
                var part = layoutParts[i];

                if (part.IsDirty)
                    part.Update(view);

                var path = part.Path;
                var pathBounds = path.GetBounds();

                if (!pathBounds.IntersectsWith(viewBounds))
                    continue;

                part.Draw(g, (i + 1).ToString());
                DrawBendLines(g, part.BasePart);
                DrawEtchMarks(g, part.BasePart);
                DrawGrainWarning(g, part.BasePart);
            }

            var previewParts = view.PreviewParts;
            var previewBrush = view.PreviewBrush;
            var previewPen = view.PreviewPen;

            for (var i = 0; i < previewParts.Count; i++)
            {
                var part = previewParts[i];

                if (part.IsDirty)
                    part.Update(view);

                var path = part.Path;
                if (!path.GetBounds().IntersectsWith(viewBounds))
                    continue;

                g.FillPath(previewBrush, path);
                g.DrawPath(previewPen, path);
            }

            if (view.DrawOffset && view.Plate.PartSpacing > 0)
                DrawOffsetGeometry(g);

            if (view.DrawBounds)
            {
                var bounds = view.SelectedParts.Select(p => p.BasePart).ToList().GetBoundingBox();
                DrawBox(g, bounds);
            }

            if (view.DrawRapid)
                DrawRapids(g);

            if (view.DrawPiercePoints)
                DrawAllPiercePoints(g);

            if (view.DrawCutDirection)
                DrawAllCutDirectionArrows(g);
        }

        public void DrawCutOffs(Graphics g)
        {
            var plate = view.Plate;
            if (plate?.CutOffs == null || plate.CutOffs.Count == 0)
                return;

            using var pen = new Pen(Color.FromArgb(64, 64, 64), 1.5f);
            using var selectedPen = new Pen(Color.FromArgb(0, 120, 255), 3.5f);

            foreach (var cutoff in plate.CutOffs)
            {
                var program = cutoff.Drawing?.Program;
                if (program == null || program.Codes.Count == 0)
                    continue;

                var activePen = view.Selection.SelectedCutOffs.Contains(cutoff) ? selectedPen : pen;

                for (var i = 0; i < program.Codes.Count - 1; i += 2)
                {
                    if (program.Codes[i] is RapidMove rapid &&
                        program.Codes[i + 1] is LinearMove linear)
                    {
                        DrawLine(g, rapid.EndPoint, linear.EndPoint, activePen);
                    }
                }
            }
        }

        public void DrawActiveWorkArea(Graphics g)
        {
            var workArea = view.ActiveWorkArea;
            if (workArea == null)
                return;

            var rect = new RectangleF
            {
                Location = view.PointWorldToGraph(workArea.Location),
                Width = view.LengthWorldToGui(workArea.Length),
                Height = view.LengthWorldToGui(workArea.Width)
            };
            rect.Y -= rect.Height;

            using var pen = new Pen(Color.Red, 1.5f)
            {
                DashStyle = DashStyle.Dash
            };
            g.DrawRectangle(pen, rect.X, rect.Y, rect.Width, rect.Height);
        }

        private static readonly Color[] PriorityFills =
        {
            Color.FromArgb(60, Color.LimeGreen),
            Color.FromArgb(60, Color.Gold),
            Color.FromArgb(60, Color.Salmon),
        };

        private static readonly Color[] PriorityBorders =
        {
            Color.FromArgb(180, Color.Green),
            Color.FromArgb(180, Color.DarkGoldenrod),
            Color.FromArgb(180, Color.DarkRed),
        };

        public void DrawDebugRemnants(Graphics g)
        {
            var remnants = view.DebugRemnants;
            if (remnants == null || remnants.Count == 0)
                return;

            for (var i = 0; i < remnants.Count; i++)
            {
                var box = remnants[i];
                var loc = view.PointWorldToGraph(box.Location);
                var w = view.LengthWorldToGui(box.Length);
                var h = view.LengthWorldToGui(box.Width);
                var rect = new RectangleF(loc.X, loc.Y - h, w, h);

                var priority = view.DebugRemnantPriorities != null && i < view.DebugRemnantPriorities.Count
                    ? System.Math.Min(view.DebugRemnantPriorities[i], 2)
                    : 0;

                using var brush = new SolidBrush(PriorityFills[priority]);
                g.FillRectangle(brush, rect);

                using var pen = new Pen(PriorityBorders[priority], 1.5f);
                g.DrawRectangle(pen, rect.X, rect.Y, rect.Width, rect.Height);

                var label = $"P{priority} {box.Width:F1}x{box.Length:F1}";
                using var font = new Font("Segoe UI", 8f);
                using var sf = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center };
                g.DrawString(label, font, Brushes.Black, rect, sf);
            }
        }

        private void DrawBendLines(Graphics g, Part part)
        {
            if (!view.ShowBendLines || part.BaseDrawing.Bends == null || part.BaseDrawing.Bends.Count == 0)
                return;

            using var bendPen = new Pen(Color.Yellow, 1.5f)
            {
                DashStyle = System.Drawing.Drawing2D.DashStyle.Dash
            };

            foreach (var bend in part.BaseDrawing.Bends)
            {
                var start = bend.StartPoint;
                var end = bend.EndPoint;

                if (part.Rotation != 0)
                {
                    start = start.Rotate(part.Rotation);
                    end = end.Rotate(part.Rotation);
                }

                start = start + part.Location;
                end = end + part.Location;

                var pt1 = view.PointWorldToGraph(start);
                var pt2 = view.PointWorldToGraph(end);

                g.DrawLine(bendPen, pt1, pt2);
            }
        }

        private void DrawEtchMarks(Graphics g, Part part)
        {
            if (!view.ShowBendLines || part.BaseDrawing.Bends == null || part.BaseDrawing.Bends.Count == 0)
                return;

            using var etchPen = new Pen(Color.Green, 1.5f);
            var etchLength = 1.0;

            foreach (var bend in part.BaseDrawing.Bends)
            {
                if (bend.Direction != BendDirection.Up)
                    continue;

                var start = bend.StartPoint;
                var end = bend.EndPoint;

                if (part.Rotation != 0)
                {
                    start = start.Rotate(part.Rotation);
                    end = end.Rotate(part.Rotation);
                }

                start = start + part.Location;
                end = end + part.Location;

                var length = bend.Length;
                var angle = bend.StartPoint.AngleTo(bend.EndPoint) + part.Rotation;

                if (length < etchLength * 3.0)
                {
                    var pt1 = view.PointWorldToGraph(start);
                    var pt2 = view.PointWorldToGraph(end);
                    g.DrawLine(etchPen, pt1, pt2);
                }
                else
                {
                    var dx = System.Math.Cos(angle) * etchLength;
                    var dy = System.Math.Sin(angle) * etchLength;

                    var s1 = view.PointWorldToGraph(start);
                    var e1 = view.PointWorldToGraph(new Vector(start.X + dx, start.Y + dy));
                    g.DrawLine(etchPen, s1, e1);

                    var s2 = view.PointWorldToGraph(end);
                    var e2 = view.PointWorldToGraph(new Vector(end.X - dx, end.Y - dy));
                    g.DrawLine(etchPen, s2, e2);
                }
            }
        }

        private void DrawGrainWarning(Graphics g, Part part)
        {
            var plate = view.Plate;
            if (!view.ShowBendLines || plate == null || part.BaseDrawing.Bends == null || part.BaseDrawing.Bends.Count == 0)
                return;

            var grainAngle = plate.GrainAngle;
            var tolerance = Angle.ToRadians(5);

            foreach (var bend in part.BaseDrawing.Bends)
            {
                var bendAngle = bend.LineAngle + part.Rotation;
                bendAngle = bendAngle % System.Math.PI;
                if (bendAngle < 0) bendAngle += System.Math.PI;

                var grainNormalized = grainAngle % System.Math.PI;
                if (grainNormalized < 0) grainNormalized += System.Math.PI;

                var diff = System.Math.Abs(bendAngle - grainNormalized);
                diff = System.Math.Min(diff, System.Math.PI - diff);

                if (diff > tolerance)
                {
                    var box = part.BaseDrawing.Program.BoundingBox();
                    var location = part.Location;
                    var pt1 = view.PointWorldToGraph(location);
                    var pt2 = view.PointWorldToGraph(new Vector(
                        location.X + box.Length, location.Y + box.Width));
                    using var warnPen = new Pen(Color.FromArgb(180, 255, 140, 0), 2f);
                    g.DrawRectangle(warnPen, pt1.X, pt2.Y,
                        System.Math.Abs(pt2.X - pt1.X), System.Math.Abs(pt2.Y - pt1.Y));
                    return;
                }
            }
        }

        private void DrawOffsetGeometry(Graphics g)
        {
            var layoutParts = view.LayoutParts;
            using var offsetPen = new Pen(Color.FromArgb(120, 255, 100, 100));

            for (var i = 0; i < layoutParts.Count; i++)
            {
                var layoutPart = layoutParts[i];

                if (layoutPart.IsDirty)
                    layoutPart.Update(view);

                layoutPart.UpdateOffset(view.Plate.PartSpacing, view.OffsetTolerance, view.Matrix);

                if (layoutPart.OffsetPath != null)
                    g.DrawPath(offsetPen, layoutPart.OffsetPath);
            }
        }

        private void DrawRapids(Graphics g)
        {
            var pen = view.ColorScheme.RapidPen;
            var pos = new Vector(0, 0);

            for (var i = 0; i < view.Plate.Parts.Count; ++i)
            {
                var part = view.Plate.Parts[i];
                var segments = RapidEnumerator.Enumerate(part.Program, part.Location, pos);

                foreach (var seg in segments)
                {
                    DrawLine(g, seg.From, seg.To, pen);
                    pos = seg.To;
                }
            }
        }

        private void DrawAllPiercePoints(Graphics g)
        {
            using var brush = new SolidBrush(Color.Red);
            using var pen = new Pen(Color.DarkRed, 1f);

            for (var i = 0; i < view.Plate.Parts.Count; ++i)
            {
                var part = view.Plate.Parts[i];
                var pgm = part.Program;
                var pos = part.Location;
                DrawProgramPiercePoints(g, pgm, part.Location, ref pos, brush, pen);
            }
        }

        private void DrawProgramPiercePoints(Graphics g, Program pgm, Vector basePos, ref Vector pos, Brush brush, Pen pen)
        {
            for (var i = 0; i < pgm.Length; ++i)
            {
                var code = pgm[i];

                if (code.Type == CodeType.SubProgramCall)
                {
                    var subpgm = (SubProgramCall)code;
                    if (subpgm.Program != null)
                    {
                        var holeBase = basePos + subpgm.Offset;
                        pos = holeBase;
                        DrawProgramPiercePoints(g, subpgm.Program, holeBase, ref pos, brush, pen);
                    }
                }
                else
                {
                    var motion = code as Motion;
                    if (motion == null) continue;

                    var endpt = pgm.Mode == Mode.Incremental
                        ? motion.EndPoint + pos
                        : motion.EndPoint + basePos;

                    if (code.Type == CodeType.RapidMove)
                    {
                        var pt = view.PointWorldToGraph(endpt);
                        var radius = 2f;
                        g.FillEllipse(brush, pt.X - radius, pt.Y - radius, radius * 2, radius * 2);
                        g.DrawEllipse(pen, pt.X - radius, pt.Y - radius, radius * 2, radius * 2);
                    }

                    pos = endpt;
                }
            }
        }

        private void DrawAllCutDirectionArrows(Graphics g)
        {
            using var pen = new Pen(Color.FromArgb(60, 60, 60), 1.5f);

            var arrowSpacingWorld = view.LengthGuiToWorld(60f);
            var arrowSize = 6f;

            for (var i = 0; i < view.Plate.Parts.Count; ++i)
            {
                var part = view.Plate.Parts[i];
                var pgm = part.Program;
                var pos = part.Location;
                CutDirectionArrows.DrawProgram(g, view, pgm, ref pos, pen, arrowSpacingWorld, arrowSize);
            }
        }

        private void DrawLine(Graphics g, Vector pt1, Vector pt2, Pen pen)
        {
            var point1 = view.PointWorldToGraph(pt1);
            var point2 = view.PointWorldToGraph(pt2);

            g.DrawLine(pen, point1, point2);
        }

        private void DrawBox(Graphics g, Box box)
        {
            var rect = new RectangleF
            {
                Location = view.PointWorldToGraph(box.Location),
                Width = view.LengthWorldToGui(box.Length),
                Height = view.LengthWorldToGui(box.Width)
            };

            g.DrawRectangle(view.ColorScheme.BoundingBoxPen, rect.X, rect.Y - rect.Height, rect.Width, rect.Height);
        }
    }
}
