using System.Drawing;
using System.Drawing.Drawing2D;
using netDxf;
using netDxf.Entities;
using Point = System.Drawing.Point;

namespace InABox.Dxf
{
    public static class DxfUtils
    {
        public static event ProcessError OnProcessError;

        // Draw a rotated string at a particular position.
        private static void DrawRotatedTextAt(Graphics gr, float angle,
            string txt, float x, float y, Font the_font, Brush the_brush)
        {
            // Save the graphics state.
            var state = gr.Save();
            gr.ResetTransform();

            // Rotate.
            gr.RotateTransform(angle);

            // Translate to desired position. Be sure to append
            // the rotation so it occurs after the rotation.
            gr.TranslateTransform(x, y, MatrixOrder.Append);

            // Draw the text at the origin.
            gr.DrawString(txt, the_font, the_brush, 0, 0);

            // Restore the graphics state.
            gr.Restore(state);
        }

        private static void CheckPoints(double x, double y, ref double minX, ref double minY, ref double maxX, ref double maxY)
        {
            minX = x < minX ? x : minX;
            minY = y < minY ? y : minY;
            maxX = x > maxX ? x : maxX;
            maxY = y > maxY ? y : maxY;
        }

        private static float Scale(int value, float offset, double scale, int border)
        {
            return (value - offset) * (float)scale + border;
        }

        private static float Scale(double value, float offset, int scale, int border)
        {
            return ((float)value - offset) * scale + border;
        }

        private static PointF VectorToPointF(Vector3 vector, PointF offset, int scale, Point border)
        {
            return new PointF(Scale(vector.X, offset.X, scale, border.X), Scale(vector.Y, offset.Y, scale, border.Y));
        }

        private static PointF VectorToPointF(Vector2 vector, PointF offset, int scale, Point border)
        {
            return new PointF(Scale(vector.X, offset.X, scale, border.X), Scale(vector.Y, offset.Y, scale, border.Y));
        }

        public delegate void ProcessError(string message);

        public static Bitmap ProcessImage(Stream stream, string caption)
        {         
            var minX = double.MaxValue;
            var minY = double.MaxValue;
            var maxX = double.MinValue;
            var maxY = double.MinValue;

            var document = DxfDocument.Load(stream);

            // Lets Explode all the various bits into their component atoms 
            // (i.e. everything is gonna be a line)
            var lines = new List<Line>();
            lines.AddRange(document.Entities.Lines);
            foreach (var polyline in document.Entities.Polylines2D)
            {
                var components = polyline.Explode();
                lines.AddRange(components.Where(x => x is Line).Select(x => x as Line));
                foreach (Arc arc in components.Where(x => x is Arc))
                    ArcToLines(lines, arc);
            }

            foreach (var polyline in document.Entities.Polylines2D)
            {
                var components = polyline.Explode();
                lines.AddRange(components.Where(x => x is Line).Select(x => x as Line));
                foreach (Arc arc in components.Where(x => x is Arc))
                    ArcToLines(lines, arc);
            }

            foreach (var arc in document.Entities.Arcs)
                ArcToLines(lines, arc);

            foreach (var ellipse in document.Entities.Ellipses)
            {
                var precision = (int)((ellipse.MajorAxis + ellipse.MinorAxis) * 2.0F * Math.PI);
                var polyline = ellipse.ToPolyline2D(precision);
                var segments = polyline.Explode();
                lines.AddRange(segments.Select(x => x as Line));
            }

            if (!lines.Any())
                throw new Exception(string.Format("No Data Found in {0}!", caption));

            foreach (var line in lines)
            {
                CheckPoints(line.StartPoint.X, line.StartPoint.Y, ref minX, ref minY, ref maxX, ref maxY);
                CheckPoints(line.EndPoint.X, line.EndPoint.Y, ref minX, ref minY, ref maxX, ref maxY);
            }

            var height = (int)Math.Abs(maxY - minY);
            var width = (int)Math.Abs(maxX - minX);

            var scale = 15;
            var margin = 100;
            var offset = new PointF((float)minX, (float)minY);
            var border = new Point(margin, margin);

            var result = new Bitmap(width * scale + margin * 2, height * scale + margin * 2);
            var pen = new Pen(new SolidBrush(Color.Black), 3.0F);
            using (var g = Graphics.FromImage(result))
            {
                Brush bg = new SolidBrush(Color.White);
                g.FillRectangle(bg, 0, 0, result.Width, result.Height);

                foreach (var line in lines)
                {
                    var start = VectorToPointF(line.StartPoint, offset, scale, border);
                    var end = VectorToPointF(line.EndPoint, offset, scale, border);
                    g.DrawLine(pen, start, end);
                }

                var font = new Font("Arial", 20.0F);
                var brush = new SolidBrush(Color.Blue);
                pen = new Pen(brush);

                var crect = g.MeasureString(caption, font);
                g.DrawString(caption, font, brush, new PointF(result.Width / 2.0F - crect.Width / 2.0F, 10));

                var heightrect = g.MeasureString(height.ToString(), font);
                g.DrawLine(pen, new PointF(result.Width - (heightrect.Height + 10.0F), 100), new PointF(result.Width - 30, 100));
                g.DrawLine(pen, new PointF(result.Width - (heightrect.Height + 10.0F), result.Height - 100),
                    new PointF(result.Width - 30, result.Height - 100));
                g.DrawLine(pen, new PointF(result.Width - (20.0F + heightrect.Height / 2.0F), 105),
                    new PointF(result.Width - (20.0F + heightrect.Height / 2.0F), result.Height - 105));

                var aX = result.Width - (20.0F + heightrect.Height / 2.0F);
                float aY = 105;
                g.FillPolygon(brush, new[]
                {
                    new(aX, aY),
                    new PointF(aX - 10.0F, aY + 10.0F),
                    new PointF(aX + 10.0F, aY + 10.0F)
                });

                aY = result.Height - 105;
                g.FillPolygon(brush, new[]
                {
                    new(aX, aY),
                    new PointF(aX - 10.0F, aY - 10.0F),
                    new PointF(aX + 10.0F, aY - 10.0F)
                });
                g.FillRectangle(bg, result.Width - (heightrect.Height + 15.0F), result.Height / 2.0F - heightrect.Width / 2.0F, heightrect.Height,
                    heightrect.Width);
                DrawRotatedTextAt(g, 270.0F, height.ToString(), result.Width - (heightrect.Height + 15.0F),
                    result.Height / 2.0F + heightrect.Width / 2.0F, font, brush);

                var widthrect = g.MeasureString(width.ToString(), font);
                g.DrawLine(pen, new PointF(100, result.Height - (widthrect.Height + 10.0F)), new PointF(100, result.Height - 30));
                g.DrawLine(pen, new PointF(result.Width - 100, result.Height - (widthrect.Height + 10.0F)),
                    new PointF(result.Width - 100, result.Height - 30));
                g.DrawLine(pen, new PointF(105, result.Height - (20.0F + widthrect.Height / 2.0F)),
                    new PointF(result.Width - 105, result.Height - (20.0F + widthrect.Height / 2.0F)));

                aX = 105;
                aY = result.Height - (20.0F + widthrect.Height / 2.0F);
                g.FillPolygon(brush, new[]
                {
                    new(aX, aY),
                    new PointF(aX + 10.0F, aY - 10.0F),
                    new PointF(aX + 10.0F, aY + 10.0F)
                });

                aX = result.Width - 105;
                g.FillPolygon(brush, new[]
                {
                    new(aX, aY),
                    new PointF(aX - 10.0F, aY - 10.0F),
                    new PointF(aX - 10.0F, aY + 10.0F)
                });

                g.FillRectangle(bg, result.Width / 2.0F - widthrect.Width / 2.0F, result.Height - (widthrect.Height + 15.0F), widthrect.Width,
                    widthrect.Height);
                g.DrawString(width.ToString(), font, brush,
                    new PointF(result.Width / 2.0F - widthrect.Width / 2.0F, result.Height - (widthrect.Height + 15.0F)));
            }

            return result;
        }

        private static void ArcToLines(List<Line> lines, Arc arc)
        {
            var precision = Math.Max(2, (int)(arc.Radius * 2.0F * Math.PI));
            var polyline = arc.ToPolyline2D(precision);
            var segments = polyline.Explode();
            lines.AddRange(segments.Select(x => x as Line));
        }

        public static Bitmap? DXFToBitmap(string filename)
        {
            using var stream = new FileStream(filename, FileMode.Open, FileAccess.Read);
            try
            {
                return ProcessImage(stream, Path.GetFileNameWithoutExtension(filename));
            }
            catch (Exception e)
            {
                OnProcessError?.Invoke(e.Message);
                return null;
            }
        }
    }
}