using OpenNest.CNC;
using OpenNest.Geometry;
using OpenNest.Math;
using System.Drawing;
using System.Drawing.Drawing2D;

namespace OpenNest
{
    internal static class GraphicsHelper
    {
        public static GraphicsPath GetGraphicsPath(this Program pgm)
        {
            var path = new GraphicsPath();
            var curpos = Vector.Zero;

            AddProgram(path, pgm, pgm.Mode, ref curpos);

            return path;
        }

        public static GraphicsPath GetGraphicsPath(this Program pgm, Vector origin)
        {
            var path = new GraphicsPath();
            var curpos = origin;

            AddProgram(path, pgm, pgm.Mode, ref curpos);

            return path;
        }

        public static GraphicsPath GetGraphicsPath(this Shape shape)
        {
            var path = new GraphicsPath();

            AddShape(path, shape);

            return path;
        }

        public static Image GetImage(this Program pgm, System.Drawing.Size size)
        {
            return pgm.GetImage(size, Pens.Black, null);
        }

        public static Image GetImage(this Program pgm, System.Drawing.Size size, Pen pen)
        {
            return pgm.GetImage(size, pen, null);
        }

        public static Image GetImage(this Program pgm, System.Drawing.Size size, Pen pen, Brush brush)
        {
            var img = new Bitmap(size.Width, size.Height);
            var path = pgm.GetGraphicsPath();
            var bounds = path.GetBounds();

            var scalex = (size.Height - 10) / bounds.Height;
            var scaley = (size.Width - 10) / bounds.Width;
            var scale = scalex < scaley ? scalex : scaley;

            var matrix = new Matrix();
            matrix.Scale(scale, -scale);

            path.Transform(matrix);

            bounds = path.GetBounds();

            var offset = new PointF(
                (size.Width - bounds.Width) * 0.5f - bounds.X,
                (size.Height - bounds.Height) * 0.5f - bounds.Y);

            var graphics = Graphics.FromImage(img);
            graphics.TranslateTransform(offset.X, offset.Y);

            if (brush != null)
                graphics.FillPath(brush, path);

            if (pen == null)
                pen = Pens.Black;

            graphics.DrawPath(pen, path);

            matrix.Dispose();
            graphics.Dispose();

            return img;
        }

        public static void GetGraphicsPaths(this Program pgm, Vector origin,
            out GraphicsPath cutPath, out GraphicsPath leadPath)
        {
            cutPath = new GraphicsPath();
            leadPath = new GraphicsPath();
            var curpos = origin;

            AddProgramSplit(cutPath, leadPath, pgm, pgm.Mode, ref curpos);
        }

        private static void AddProgramSplit(GraphicsPath cutPath, GraphicsPath leadPath,
            Program pgm, Mode mode, ref Vector curpos)
        {
            // Capture the frame origin at entry. Sub-program Offsets are relative
            // to this fixed origin, not to the current tool position.
            var frameOrigin = curpos;
            mode = pgm.Mode;

            for (var i = 0; i < pgm.Length; ++i)
            {
                var code = pgm[i];

                switch (code.Type)
                {
                    case CodeType.ArcMove:
                        var arc = (ArcMove)code;
                        if (arc.Suppressed)
                        {
                            var endpt = arc.EndPoint;
                            if (mode == Mode.Incremental) endpt += curpos;
                            curpos = endpt;
                            break;
                        }
                        var arcPath = (arc.Layer == LayerType.Leadin || arc.Layer == LayerType.Leadout)
                            ? leadPath : cutPath;
                        AddArc(arcPath, arc, mode, ref curpos);
                        break;

                    case CodeType.LinearMove:
                        var line = (LinearMove)code;
                        if (line.Suppressed)
                        {
                            var endpt = line.EndPoint;
                            if (mode == Mode.Incremental) endpt += curpos;
                            curpos = endpt;
                            break;
                        }
                        var linePath = (line.Layer == LayerType.Leadin || line.Layer == LayerType.Leadout)
                            ? leadPath : cutPath;
                        AddLine(linePath, line, mode, ref curpos);
                        break;

                    case CodeType.RapidMove:
                        {
                            var rapid = (RapidMove)code;
                            var endpt = rapid.EndPoint;
                            if (mode == Mode.Incremental)
                                endpt += curpos;
                            var dx = endpt.X - curpos.X;
                            var dy = endpt.Y - curpos.Y;
                            if (dx * dx + dy * dy > 0.001 * 0.001)
                            {
                                cutPath.StartFigure();
                                leadPath.StartFigure();
                            }
                            curpos = endpt;
                        }
                        break;

                    case CodeType.SubProgramCall:
                        var tmpmode = mode;
                        var subpgm = (SubProgramCall)code;
                        if (subpgm.Program != null)
                        {
                            cutPath.StartFigure();
                            leadPath.StartFigure();
                            curpos = new Vector(frameOrigin.X + subpgm.Offset.X, frameOrigin.Y + subpgm.Offset.Y);
                            AddProgramSplit(cutPath, leadPath, subpgm.Program, mode, ref curpos);
                        }
                        mode = tmpmode;
                        break;
                }
            }
        }

        private static void AddArc(GraphicsPath path, ArcMove arc, Mode mode, ref Vector curpos)
        {
            var endpt = arc.EndPoint;
            var center = arc.CenterPoint;

            if (mode == Mode.Incremental)
            {
                endpt += curpos;
                center += curpos;
            }

            // start angle in degrees
            var startAngle = Angle.ToDegrees(System.Math.Atan2(
                curpos.Y - center.Y,
                curpos.X - center.X));

            // end angle in degrees
            var endAngle = Angle.ToDegrees(System.Math.Atan2(
                endpt.Y - center.Y,
                endpt.X - center.X));

            endAngle = Angle.NormalizeDeg(endAngle);
            startAngle = Angle.NormalizeDeg(startAngle);

            if (arc.Rotation == RotationType.CCW && endAngle < startAngle)
                endAngle += 360.0;
            else if (arc.Rotation == RotationType.CW && startAngle < endAngle)
                startAngle += 360.0;

            var dx = endpt.X - center.X;
            var dy = endpt.Y - center.Y;

            var radius = System.Math.Sqrt(dx * dx + dy * dy);

            var pt = new PointF((float)(center.X - radius), (float)(center.Y - radius));
            var size = (float)(radius * 2.0);

            if (startAngle.IsEqualTo(endAngle))
            {
                path.AddEllipse(pt.X, pt.Y, size, size);
            }
            else
            {
                var sweepAngle = (endAngle - startAngle);

                path.AddArc(
                    pt.X, pt.Y,
                    size, size,
                    (float)startAngle,
                    (float)sweepAngle);
            }

            curpos = endpt;
        }

        private static void AddLine(GraphicsPath path, LinearMove line, Mode mode, ref Vector curpos)
        {
            var pt = line.EndPoint;

            if (mode == Mode.Incremental)
                pt += curpos;

            var pt1 = new PointF((float)curpos.X, (float)curpos.Y);
            var pt2 = new PointF((float)pt.X, (float)pt.Y);

            path.AddLine(pt1, pt2);

            curpos = pt;
        }

        private static void AddLine(GraphicsPath path, RapidMove line, Mode mode, ref Vector curpos)
        {
            var pt = line.EndPoint;

            if (mode == Mode.Incremental)
                pt += curpos;

            curpos = pt;
        }

        private static void AddProgram(GraphicsPath path, Program pgm, Mode mode, ref Vector curpos)
        {
            // Capture the frame origin at entry. Sub-program Offsets are relative
            // to this fixed origin, not to the current tool position.
            var frameOrigin = curpos;
            mode = pgm.Mode;
            GraphicsPath currentFigure = null;

            void Flush()
            {
                if (currentFigure != null)
                {
                    path.AddPath(currentFigure, false);
                    currentFigure.Dispose();
                    currentFigure = null;
                }
            }

            for (int i = 0; i < pgm.Length; ++i)
            {
                var code = pgm[i];

                switch (code.Type)
                {
                    case CodeType.ArcMove:
                        {
                            var arc = (ArcMove)code;
                            if (arc.Layer != LayerType.Leadin && arc.Layer != LayerType.Leadout)
                            {
                                if (currentFigure == null) currentFigure = new GraphicsPath();
                                AddArc(currentFigure, arc, mode, ref curpos);
                            }
                            else
                            {
                                Flush();
                                var endpt = arc.EndPoint;
                                if (mode == Mode.Incremental) endpt += curpos;
                                curpos = endpt;
                            }
                        }
                        break;

                    case CodeType.LinearMove:
                        {
                            var line = (LinearMove)code;
                            if (line.Layer != LayerType.Leadin && line.Layer != LayerType.Leadout)
                            {
                                if (currentFigure == null) currentFigure = new GraphicsPath();
                                AddLine(currentFigure, line, mode, ref curpos);
                            }
                            else
                            {
                                Flush();
                                var endpt = line.EndPoint;
                                if (mode == Mode.Incremental) endpt += curpos;
                                curpos = endpt;
                            }
                        }
                        break;

                    case CodeType.RapidMove:
                        {
                            var rapid = (RapidMove)code;
                            var endpt = rapid.EndPoint;
                            if (mode == Mode.Incremental)
                                endpt += curpos;
                            var dx = endpt.X - curpos.X;
                            var dy = endpt.Y - curpos.Y;
                            if (dx * dx + dy * dy > 0.001 * 0.001)
                                Flush();
                            curpos = endpt;
                        }
                        break;

                    case CodeType.SubProgramCall:
                        {
                            Flush();
                            var tmpmode = mode;
                            var subpgm = (SubProgramCall)code;

                            if (subpgm.Program != null)
                            {
                                curpos = new Vector(frameOrigin.X + subpgm.Offset.X, frameOrigin.Y + subpgm.Offset.Y);
                                AddProgram(path, subpgm.Program, mode, ref curpos);
                            }

                            mode = tmpmode;
                            break;
                        }
                }
            }

            Flush();
        }

        private static void AddArc(GraphicsPath path, Arc arc)
        {
            var diameter = arc.Diameter;

            var endAngle = Angle.NormalizeDeg(Angle.ToDegrees(arc.EndAngle));
            var startAngle = Angle.NormalizeDeg(Angle.ToDegrees(arc.StartAngle));

            if (arc.Rotation == RotationType.CCW && endAngle < startAngle)
                endAngle += 360.0;
            else if (arc.Rotation == RotationType.CW && startAngle < endAngle)
                startAngle += 360.0;

            var sweepAngle = (endAngle - startAngle);

            path.AddArc(
                (float)(arc.Center.X - arc.Radius),
                (float)(arc.Center.Y - arc.Radius),
                (float)diameter,
                (float)diameter,
                (float)(startAngle),
                (float)sweepAngle);
        }

        private static void AddCircle(GraphicsPath path, Circle circle)
        {
            var diameter = circle.Diameter;

            path.AddEllipse(
                (float)(circle.Center.X - circle.Radius),
                (float)(circle.Center.Y - circle.Radius),
                (float)diameter,
                (float)diameter);
        }

        private static void AddLine(GraphicsPath path, Line line)
        {
            path.AddLine(
                (float)line.StartPoint.X,
                (float)line.StartPoint.Y,
                (float)line.EndPoint.X,
                (float)line.EndPoint.Y);
        }

        private static void AddShape(GraphicsPath path, Shape shape)
        {
            foreach (var entity in shape.Entities)
            {
                if (entity.Layer != null)
                {
                    if (string.Equals(entity.Layer.Name, SpecialLayers.Leadin.Name, System.StringComparison.OrdinalIgnoreCase) ||
                        string.Equals(entity.Layer.Name, SpecialLayers.Leadout.Name, System.StringComparison.OrdinalIgnoreCase))
                    {
                        continue;
                    }
                }

                switch (entity.Type)
                {
                    case EntityType.Arc:
                        AddArc(path, (Arc)entity);
                        break;

                    case EntityType.Circle:
                        AddCircle(path, (Circle)entity);
                        break;

                    case EntityType.Line:
                        AddLine(path, (Line)entity);
                        break;

                    case EntityType.Polygon:
                        AddPolygon(path, (Polygon)entity);
                        break;

                    case EntityType.Shape:
                        var subpath = new GraphicsPath();
                        AddShape(subpath, (Shape)entity);
                        path.AddPath(subpath, false);
                        subpath.Dispose();
                        break;
                }
            }
        }

        private static void AddPolygon(GraphicsPath path, Polygon polygon)
        {
            var pts = new PointF[polygon.Vertices.Count];

            for (int i = 0; i < pts.Length; i++)
            {
                var pt = polygon.Vertices[i];
                pts[i] = new PointF((float)pt.X, (float)pt.Y);
            }

            path.AddPolygon(pts);
        }
    }
}
