using InABox.Core;
using netDxf;
using netDxf.Blocks;
using netDxf.Entities;
using netDxf.Tables;
using Syncfusion.XPS;
using System;
using System.Collections.Generic;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using FontStyle = System.Drawing.FontStyle;
using SVec2 = System.Numerics.Vector2;
using TAlignment = netDxf.Entities.TextAlignment;

namespace InABox.Dxf;

internal interface IDxfObject
{
    void Draw(DrawData data);

    RectangleF? GetBounds(TransformData data);
}

internal class DxfLine : IDxfObject
{
    public Line Line { get; set; }

    public void Draw(DrawData data)
    {
        if (!data.Data.ShouldDraw(Line)) return;

        data.PushTransform();
        //data.ArbitraryAxis(Line.Normal);

        var colour = data.ResolveColour(Line.Color, Line);
        var thickness = (float)Line.Thickness;

        if(Line.Linetype.Segments.Count > 0)
        {
            var pos = DrawData.ConvertPoint(Line.StartPoint).ToVector2();
            var end = DrawData.ConvertPoint(Line.EndPoint).ToVector2();

            var remainingLength = SVec2.Distance(pos, end);
            if(remainingLength > 0)
            {
                var dir = SVec2.Normalize(end - pos);

                while(remainingLength > 0)
                {
                    foreach(var segment in Line.Linetype.Segments)
                    {
                        if(segment is LinetypeSimpleSegment simple)
                        {
                            if(simple.Length > 0)
                            {
                                var length = Math.Min(remainingLength, (float)(simple.Length * Line.LinetypeScale));
                                data.Graphics.DrawLine(
                                    colour, thickness,
                                    new PointF(pos), new PointF(pos + dir * length));
                                remainingLength -= length;
                                pos += dir * length;
                            }
                            else if(simple.Length == 0)
                            {
                                data.Graphics.DrawPoint(colour, thickness, new PointF(pos));
                            }
                            else
                            {
                                var length = (float)(-simple.Length * Line.LinetypeScale);
                                remainingLength -= length;
                                pos += dir * length;
                            }
                        }
                        if(remainingLength <= 0)
                        {
                            break;
                        }
                    }
                }
            }
        }
        else
        {
            data.Graphics.DrawLine(
                colour, thickness,
                DrawData.ConvertPoint(Line.StartPoint), DrawData.ConvertPoint(Line.EndPoint));
        }

        data.PopTransform();
    }

    public RectangleF? GetBounds(TransformData data)
    {
        if (!data.Data.ShouldDraw(Line)) return null;

        return Utils.RectangleFromPoints(
            data.TransformPoint(Line.StartPoint),
            data.TransformPoint(Line.EndPoint));
    }
}

internal class DxfInsert : IDxfObject
{
    public Insert Insert { get; set; }

    public List<IDxfObject> Objects { get; set; }

    public DxfInsert(Insert insert)
    {
        Insert = insert;
        Objects = insert.Block.Entities.Select(DxfUtils.ConvertEl).NotNull().ToList();
    }

    public void Draw(DrawData data)
    {
        if (!data.Data.ShouldDraw(Insert)) return;

        // var transformation = Insert.GetTransformation();
        // var translation = Insert.Position - transformation * Insert.Block.Origin;

        data.PushTransform();
        data.Translate(new PointF((float)Insert.Position.X, (float)Insert.Position.Y));
        data.ArbitraryAxis(Insert.Normal);
        data.Rotate((float)Insert.Rotation);
        data.Scale((float)Insert.Scale.X, (float)Insert.Scale.Y);
        data.Translate(new PointF(-(float)Insert.Block.Origin.X, -(float)Insert.Block.Origin.Y));

        data.PushBlockColour(Insert.Color, Insert);

        foreach(var entity in Objects)
        {
            entity.Draw(data);
        }

        data.PopBlockColour();

        data.PopTransform();

        // foreach(var entity in Insert.Explode())
        // {
        //     // var ent = entity.Clone() as EntityObject;
        //     // ent.TransformBy(transformation, translation);
        //     DxfUtils.ConvertEl(entity)?.Draw(data);
        // }

        // foreach(var obj in Insert.Explode())
        // {
        //     DxfUtils.ConvertEl(obj)?.Draw(data);
        // }
    }

    public RectangleF? GetBounds(TransformData data)
    {
        if (!data.Data.ShouldDraw(Insert)) return null;

        data.PushTransform();
        data.Translate(new PointF((float)Insert.Position.X, (float)Insert.Position.Y));
        data.ArbitraryAxis(Insert.Normal);
        data.Rotate((float)Insert.Rotation);
        data.Scale((float)Insert.Scale.X, (float)Insert.Scale.Y);
        data.Translate(new PointF(-(float)Insert.Block.Origin.X, -(float)Insert.Block.Origin.Y));

        var bounds = Utils.CombineBounds(Objects.Select(x => x.GetBounds(data)));

        data.PopTransform();

        return bounds;
    }
}

internal class DxfArc : IDxfObject
{
    public Arc Arc { get; set; }

    public DxfArc(Arc arc)
    {
        Arc = arc;
    }

    public void Draw(DrawData data)
    {
        if (!data.Data.ShouldDraw(Arc)) return;

        foreach(var obj in Arc.ToPolyline2D(100).Explode())
        {
            DxfUtils.ConvertEl(obj)?.Draw(data);
        }
    }

    public RectangleF? GetBounds(TransformData data)
    {
        if (!data.Data.ShouldDraw(Arc)) return null;

        var halfSize = new netDxf.Vector3(Arc.Radius, Arc.Radius, 0);
        return Utils.RectangleFromPoints(
            data.TransformPoint(Arc.Center - halfSize),
            data.TransformPoint(Arc.Center + halfSize));
    }
}

internal class DxfEllipse : IDxfObject
{
    public Ellipse Ellipse { get; set; }

    public DxfEllipse(Ellipse ellipse)
    {
        Ellipse = ellipse;
    }

    public void Draw(DrawData data)
    {
        if (!data.Data.ShouldDraw(Ellipse)) return;

        foreach(var obj in Ellipse.ToPolyline2D(100).Explode())
        {
            DxfUtils.ConvertEl(obj)?.Draw(data);
        }
        // var center = DrawData.ConvertPoint(Ellipse.Center);
        // var size = new SizeF((float)Ellipse.MajorAxis, (float)Ellipse.MinorAxis);

        // var startAngle = (float)(Ellipse.StartAngle);
        // var endAngle = (float)(Ellipse.EndAngle);

        // data.Graphics.DrawArc(new Pen(Color.Black, data.ConvertThickness((float)Ellipse.Thickness)), center.X - size.Width / 2, center.Y - size.Height / 2, size.Width, size.Height, startAngle, Utils.Mod(endAngle - startAngle, 360));
    }

    public RectangleF? GetBounds(TransformData data)
    {
        if (!data.Data.ShouldDraw(Ellipse)) return null;

        var halfSize = new netDxf.Vector3(Ellipse.MajorAxis / 2, Ellipse.MinorAxis / 2, 0);
        return Utils.RectangleFromPoints(
            data.TransformPoint(Ellipse.Center - halfSize),
            data.TransformPoint(Ellipse.Center + halfSize));
    }
}

internal class DxfSolid : IDxfObject
{
    public Solid Solid { get; set; }

    public DxfSolid(Solid solid)
    {
        Solid = solid;
    }

    public void Draw(DrawData data)
    {
        if (!data.Data.ShouldDraw(Solid)) return;

        var vertices = new Vector2[]
        {
            Solid.FirstVertex,
            Solid.SecondVertex,
            Solid.FourthVertex, // Apparently the third and fourth are the wrong way round, so I've mirrored that here.
            Solid.ThirdVertex
        };
        data.Graphics.FillPolygon(data.ResolveColour(Solid.Color, Solid), vertices.ToArray(x => DrawData.ConvertPoint(x)));
    }

    public RectangleF? GetBounds(TransformData data)
    {
        if (!data.Data.ShouldDraw(Solid)) return null;

        var vertices = new Vector2[]
        {
            Solid.FirstVertex,
            Solid.SecondVertex,
            Solid.FourthVertex, // Apparently the third and fourth are the wrong way round, so I've mirrored that here.
            Solid.ThirdVertex
        };
        return Utils.RectangleFromPoints(
            vertices.ToArray(data.TransformPoint));
    }
}

internal class DxfPolyline2D : IDxfObject
{
    public Polyline2D Polyline { get; set; }

    public DxfPolyline2D(Polyline2D polyline)
    {
        Polyline = polyline;
    }

    public void Draw(DrawData data)
    {
        if (!data.Data.ShouldDraw(Polyline)) return;

        var entities = Polyline.Explode();
        foreach(var entity in entities)
        {
            DxfUtils.ConvertEl(entity)?.Draw(data);
        }

        // if(Polyline.SmoothType == PolylineSmoothType.NoSmooth)
        // {
        //     var vertices = Polyline.Vertexes.ToArray(x => new PointF((float)x.Position.X, (float)x.Position.Y));
        //     if (Polyline.IsClosed)
        //     {
        //         data.Graphics.DrawPolygon(
        //             new Pen(Color.Black, data.ConvertThickness((float)Polyline.Thickness)),
        //             vertices);
        //     }
        //     else
        //     {
        //         data.Graphics.DrawLines(
        //             new Pen(Color.Black, data.ConvertThickness((float)Polyline.Thickness)),
        //             vertices);
        //     }
        // }
        // else
        // {

        // }
    }

    public RectangleF? GetBounds(TransformData data)
    {
        if (!data.Data.ShouldDraw(Polyline)) return null;

        var entities = Polyline.Explode();
        return Utils.CombineBounds(entities.Select(x => DxfUtils.ConvertEl(x)?.GetBounds(data)));
    }
}

internal class DxfText : IDxfObject
{
    public Text Text { get; set; }

    public DxfText(Text text)
    {
        Text = text;
    }

    private Font GetFont()
    {
        FontFamily fontFamily;
        if (Text.Style.FontFamilyName.IsNullOrWhiteSpace())
        {
            fontFamily = SystemFonts.DefaultFont.FontFamily;
        }
        else
        {
            fontFamily = new FontFamily(Text.Style.FontFamilyName);
        }
        return new Font(fontFamily, (float)Text.Height, Text.Style.FontStyle switch
        {
            netDxf.Tables.FontStyle.Bold => FontStyle.Bold,
            netDxf.Tables.FontStyle.Italic => FontStyle.Italic,
            netDxf.Tables.FontStyle.Regular or _ => FontStyle.Regular,
        });
    }

    private string GetText()
    {
        return Text.Value;
    }

    private static Bitmap _placeholderBitmap = new Bitmap(1, 1);

    private void Transform(TransformData data, Font font, string text, Graphics g)
    {
        data.Translate(new PointF((float)Text.Position.X, (float)Text.Position.Y));
        data.Rotate((float)Text.Rotation);
        data.Scale(1, -1);

        var size = g.MeasureString(text, font, new PointF(), StringFormat.GenericTypographic);
        if(Text.Alignment == TAlignment.Aligned)
        {
            data.Scale((float)(Text.Width / size.Width));
        }
        else if(Text.Alignment == TAlignment.Fit)
        {
            data.Scale((float)(Text.Width / size.Width), 1);
        }
        else
        {
            switch (Text.Alignment)
            {
                case TAlignment.MiddleLeft:
                case TAlignment.MiddleCenter:
                case TAlignment.Middle:
                case TAlignment.MiddleRight:
                    data.Translate(new PointF(0, -size.Height / 2));
                    break;
                case TAlignment.BottomLeft:
                case TAlignment.BottomCenter:
                case TAlignment.BottomRight:
                    data.Translate(new PointF(0, -size.Height));
                    break;
                default:
                    var ascent = font.FontFamily.GetCellAscent(font.Style);
                    var lineSpace = font.FontFamily.GetLineSpacing(font.Style);
                    var baseline = ascent * font.Height / font.FontFamily.GetEmHeight(font.Style);
                    var ratio = font.GetHeight(g) / lineSpace;
                    data.Translate(new PointF(0, -baseline + ascent * ratio));
                    break;
            }

            switch (Text.Alignment)
            {
                case TAlignment.TopLeft:
                case TAlignment.MiddleLeft:
                case TAlignment.BottomLeft:
                    break;
                case TAlignment.TopCenter:
                case TAlignment.MiddleCenter:
                case TAlignment.Middle:
                case TAlignment.BottomCenter:
                    data.Translate(new PointF(-(float)size.Width / 2, 0));
                    break;
                case TAlignment.TopRight:
                case TAlignment.MiddleRight:
                case TAlignment.BottomRight:
                    data.Translate(new PointF(-(float)size.Width, 0));
                    break;
            }
        }
    }

    public void Draw(DrawData data)
    {
        if (!data.Data.ShouldDraw(Text)) return;

        var text = GetText();
        if (Text.Style.FontFamilyName.IsNullOrWhiteSpace())
        {
            data.Graphics.SetFont((float)Text.Height, Text.Style.FontStyle switch
            {
                netDxf.Tables.FontStyle.Bold => FontStyle.Bold,
                netDxf.Tables.FontStyle.Italic => FontStyle.Italic,
                netDxf.Tables.FontStyle.Regular or _ => FontStyle.Regular,
            });
        }
        else
        {
            data.Graphics.SetFont(Text.Style.FontFamilyName, (float)Text.Height, Text.Style.FontStyle switch
            {
                netDxf.Tables.FontStyle.Bold => FontStyle.Bold,
                netDxf.Tables.FontStyle.Italic => FontStyle.Italic,
                netDxf.Tables.FontStyle.Regular or _ => FontStyle.Regular,
            });
        }

        if(Text.Alignment == TAlignment.Aligned || Text.Alignment == TAlignment.Fit)
        {
            // Need to calculate a rectangle to draw the text in.
            var scale = new PointF(Text.IsBackward ? -1 : 1, Text.IsUpsideDown ? -1 : 1);
            if(Text.Alignment == TAlignment.Aligned)
            {
                data.Graphics.DrawText(text, data.ResolveColour(Text.Color, Text), DrawData.ConvertPoint(Text.Position), (float)Text.Rotation,
                    new SizeF((float)(Text.Width * Text.WidthFactor), (float)Text.Height), scale);
            }
            else
            {
                data.Graphics.DrawText(text, data.ResolveColour(Text.Color, Text), DrawData.ConvertPoint(Text.Position), (float)Text.Rotation,
                    (float)(Text.Width * Text.WidthFactor), scale);
            }
        }
        else
        {
            var format = new TextFormat
            {
                LineAlignment = Text.Alignment switch
                {
                    TAlignment.Aligned => TextLineAlignment.Baseline,
                    TAlignment.Fit => TextLineAlignment.Baseline,
                    TAlignment.MiddleLeft or TAlignment.MiddleCenter or TAlignment.MiddleRight or TAlignment.Middle => TextLineAlignment.Center,
                    TAlignment.BottomLeft or TAlignment.BottomCenter or TAlignment.BottomRight => TextLineAlignment.Bottom,
                    TAlignment.BaselineLeft or TAlignment.BaselineRight or TAlignment.BaselineCenter => TextLineAlignment.Baseline,
                    _ => TextLineAlignment.Top,
                },
                Alignment = Text.Alignment switch
                {
                    TAlignment.Aligned => TextAlignment.Justify,
                    TAlignment.Fit => TextAlignment.Fit,
                    TAlignment.TopCenter or TAlignment.MiddleCenter or TAlignment.BottomCenter or TAlignment.Middle or TAlignment.BaselineCenter => TextAlignment.Center,
                    TAlignment.TopRight or TAlignment.MiddleRight or TAlignment.BottomRight or TAlignment.BaselineRight => TextAlignment.Right,
                    _ => TextAlignment.Left,
                }
            };

            data.PushTransform();
            data.Translate(DrawData.ConvertPoint(Text.Position));
            data.Rotate((float)Text.Rotation);
            data.Scale((float)Text.WidthFactor, 1);
            data.Scale(Text.IsBackward ? -1 : 1, Text.IsUpsideDown ? -1 : 1);
            data.Graphics.DrawText(text, data.ResolveColour(Text.Color, Text), new(), 0, format);
            data.PopTransform();
        }
    }

    public RectangleF? GetBounds(TransformData data)
    {
        if (!data.Data.ShouldDraw(Text)) return null;

        var font = GetFont();
        var text = GetText();

        data.PushTransform();
        using var g = Graphics.FromImage(_placeholderBitmap);
        Transform(data, font, text, g);

        var size = g.MeasureString(text, font, new PointF(), StringFormat.GenericTypographic);
        var bounds = Utils.RectangleFromPoints(
            data.TransformPoint(0, 0),
            data.TransformPoint(size.Width, size.Height));

        data.PopTransform();
        return bounds;
    }
}

internal class DxfMText : IDxfObject
{
    public MText MText { get; set; }

    public DxfMText(MText text)
    {
        MText = text;
    }

    private Font GetFont()
    {
        FontFamily fontFamily;
        if (MText.Style.FontFamilyName.IsNullOrWhiteSpace())
        {
            fontFamily = SystemFonts.DefaultFont.FontFamily;
        }
        else
        {
            fontFamily = new FontFamily(MText.Style.FontFamilyName);
        }
        return new Font(fontFamily, (float)MText.Height, MText.Style.FontStyle switch
        {
            netDxf.Tables.FontStyle.Bold => FontStyle.Bold,
            netDxf.Tables.FontStyle.Italic => FontStyle.Italic,
            netDxf.Tables.FontStyle.Regular or _ => FontStyle.Regular,
        });
    }

    private string GetText()
    {
        return MText.PlainText().Replace("^M", "");
    }

    private static Bitmap _placeholderBitmap = new Bitmap(1, 1);

    private void Transform(TransformData data, Font font, string text, Graphics g)
    {
        data.Translate(new PointF((float)MText.Position.X, (float)MText.Position.Y));
        data.Rotate((float)MText.Rotation);
        data.Scale(1, -1);

        var size = g.MeasureString(text, font, new PointF(), StringFormat.GenericTypographic);
        switch (MText.AttachmentPoint)
        {
            case MTextAttachmentPoint.MiddleLeft:
            case MTextAttachmentPoint.MiddleCenter:
            case MTextAttachmentPoint.MiddleRight:
                data.Translate(new PointF(0, -size.Height / 2));
                break;
            case MTextAttachmentPoint.BottomLeft:
            case MTextAttachmentPoint.BottomCenter:
            case MTextAttachmentPoint.BottomRight:
                data.Translate(new PointF(0, -size.Height));
                break;
            default:
                var ascent = font.FontFamily.GetCellAscent(font.Style);
                var lineSpace = font.FontFamily.GetLineSpacing(font.Style);
                var baseline = ascent * font.Height / font.FontFamily.GetEmHeight(font.Style);
                var ratio = font.GetHeight(g) / lineSpace;
                data.Translate(new PointF(0, -baseline + ascent * ratio));
                break;
        }

        switch (MText.AttachmentPoint)
        {
            case MTextAttachmentPoint.TopLeft:
            case MTextAttachmentPoint.MiddleLeft:
            case MTextAttachmentPoint.BottomLeft:
                break;
            case MTextAttachmentPoint.TopCenter:
            case MTextAttachmentPoint.MiddleCenter:
            case MTextAttachmentPoint.BottomCenter:
                data.Translate(new PointF(-(float)size.Width / 2, 0));
                break;
            case MTextAttachmentPoint.TopRight:
            case MTextAttachmentPoint.MiddleRight:
            case MTextAttachmentPoint.BottomRight:
                data.Translate(new PointF(-(float)size.Width, 0));
                break;
        }
    }

    public void Draw(DrawData data)
    {
        if (!data.Data.ShouldDraw(MText)) return;

        var text = GetText();
        if (MText.Style.FontFamilyName.IsNullOrWhiteSpace())
        {
            data.Graphics.SetFont((float)MText.Height, MText.Style.FontStyle switch
            {
                netDxf.Tables.FontStyle.Bold => FontStyle.Bold,
                netDxf.Tables.FontStyle.Italic => FontStyle.Italic,
                netDxf.Tables.FontStyle.Regular or _ => FontStyle.Regular,
            });
        }
        else
        {
            data.Graphics.SetFont(MText.Style.FontFamilyName, (float)MText.Height, MText.Style.FontStyle switch
            {
                netDxf.Tables.FontStyle.Bold => FontStyle.Bold,
                netDxf.Tables.FontStyle.Italic => FontStyle.Italic,
                netDxf.Tables.FontStyle.Regular or _ => FontStyle.Regular,
            });
        }

        var format = new TextFormat
        {
            LineAlignment = MText.AttachmentPoint switch
            {
                MTextAttachmentPoint.MiddleLeft or MTextAttachmentPoint.MiddleCenter or MTextAttachmentPoint.MiddleRight => TextLineAlignment.Center,
                MTextAttachmentPoint.BottomLeft or MTextAttachmentPoint.BottomCenter or MTextAttachmentPoint.BottomRight => TextLineAlignment.Bottom,
                _ => TextLineAlignment.Top,
            },
            Alignment = MText.AttachmentPoint switch
            {
                MTextAttachmentPoint.TopCenter or MTextAttachmentPoint.MiddleCenter or MTextAttachmentPoint.BottomCenter => TextAlignment.Center,
                MTextAttachmentPoint.TopRight or MTextAttachmentPoint.MiddleRight or MTextAttachmentPoint.BottomRight => TextAlignment.Right,
                _ => TextAlignment.Left,
            }
        };
        data.Graphics.DrawText(text, data.ResolveColour(MText.Color, MText), DrawData.ConvertPoint(MText.Position), (float)MText.Rotation, format);
    }

    public RectangleF? GetBounds(TransformData data)
    {
        if (!data.Data.ShouldDraw(MText)) return null;

        var font = GetFont();
        var text = GetText();

        data.PushTransform();
        using var g = Graphics.FromImage(_placeholderBitmap);
        Transform(data, font, text, g);

        var size = g.MeasureString(text, font, new PointF(), StringFormat.GenericTypographic);
        var bounds = Utils.RectangleFromPoints(
            data.TransformPoint(0, 0),
            data.TransformPoint(size.Width, size.Height));

        data.PopTransform();
        return bounds;
    }
}

internal class DxfDimension : IDxfObject
{
    public Dimension Dimension { get; set; }

    public List<IDxfObject> Objects { get; set; }

    public DxfDimension(Dimension dimension)
    {
        Dimension = dimension;

        if(Dimension.Block is null)
        {
            Objects = new();
        }
        else
        {
            Objects = Dimension.Block.Entities.Select(DxfUtils.ConvertEl).NotNull().ToList();
        }
    }

    public void Draw(DrawData data)
    {
        if (!data.Data.ShouldDraw(Dimension)) return;

        data.PushBlockColour(Dimension.Color, Dimension);
        foreach(var entity in Objects)
        {
            entity.Draw(data);
        }
        data.PopBlockColour();
    }

    public RectangleF? GetBounds(TransformData data)
    {
        if (!data.Data.ShouldDraw(Dimension)) return null;

        return Utils.CombineBounds(Objects.Select(x => x.GetBounds(data)));
    }
}
