using InABox.Core;
using netDxf;
using netDxf.Tables;
using Svg;
using Svg.Transforms;
using Syncfusion.Pdf;
using Syncfusion.Pdf.Graphics;
using Syncfusion.Pdf.Parsing;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Linq;
using System.Numerics;
using System.Text;
using System.Threading.Tasks;
using FontStyle = System.Drawing.FontStyle;
using Vector2 = System.Numerics.Vector2;
using Vector3 = System.Numerics.Vector3;
using Vector4 = System.Numerics.Vector4;
using SPdfGraphics = Syncfusion.Pdf.Graphics.PdfGraphics;
using Syncfusion.Pdf.Tables;
using netDxf.Entities;

namespace InABox.Dxf;

public enum TextAlignment
{
    Left,
    Center,
    Right,
    Fit,
    Justify
}

public enum TextLineAlignment
{
    Top,
    Center,
    Bottom,
    Baseline
}

public class TextFormat
{
    public TextAlignment Alignment { get; set; } = TextAlignment.Left;

    public TextLineAlignment LineAlignment { get; set; } = TextLineAlignment.Top;
}

public class TransformData
{
    public DxfData Data { get; set; }

    private Stack<Matrix4x4> MatrixStack = new();

    public Matrix4x4 Transform { get; private set; } = Matrix4x4.Identity;

    public void PushTransform()
    {
        MatrixStack.Push(Transform);
    }

    public void TransformBy(Matrix4x4 matrix)
    {
        Transform = matrix * Transform;
    }

    protected virtual void UpdateTransform()
    {
    }

    protected Matrix ProjectMatrix(Matrix4x4 matrix)
    {
        var elMatrix = new Matrix3x2();
        elMatrix.M11 = (float)matrix.M11;
        elMatrix.M12 = (float)matrix.M12;
        elMatrix.M21 = (float)matrix.M21;
        elMatrix.M22 = (float)matrix.M22;
        elMatrix.M31 = (float)matrix.M41;
        elMatrix.M32 = (float)matrix.M42;
        var newMatrix = new Matrix();
        newMatrix.MatrixElements = elMatrix;
        return newMatrix;
    }

    public void PopTransform()
    {
        Transform = MatrixStack.Pop();
        UpdateTransform();
    }

    public static Matrix4x4 ArbitraryAxisMatrix(Vector3 zAxis)
    {
        if (zAxis.Equals(Vector3.UnitZ))
        {
            return Matrix4x4.Identity;
        }

        var unitY = Vector3.UnitY;
        var unitZ = Vector3.UnitZ;
        var v = ((!(Math.Abs(zAxis.X) < 1.0 / 64.0) || !(Math.Abs(zAxis.Y) < 1.0 / 64.0)) ? Vector3.Cross(unitZ, zAxis) : Vector3.Cross(unitY, zAxis));
        v = Vector3.Normalize(v);
        var vector = Vector3.Cross(zAxis, v);
        vector = Vector3.Normalize(vector);
        return new Matrix4x4(v.X, vector.X, zAxis.X, 0, v.Y, vector.Y, zAxis.Y, 0, v.Z, vector.Z, zAxis.Z, 0, 0, 0, 0, 1);
    }
    public void ArbitraryAxis(netDxf.Vector3 zAxis)
    {
        Transform = ArbitraryAxisMatrix(new((float)zAxis.X, (float)zAxis.Y, (float)zAxis.Z)) * Transform;
        UpdateTransform();
    }
    public void ArbitraryAxis(Vector3 zAxis)
    {
        Transform = ArbitraryAxisMatrix(zAxis) * Transform;
        UpdateTransform();
    }

    public void Translate(float x, float y)
    {
        Transform = Transform.Translate(x, y, 0);
        UpdateTransform();
    }
    public void Translate(PointF point)
    {
        Transform = Transform.Translate(point.X, point.Y, 0);
        UpdateTransform();
    }

    public void Rotate(float angle)
    {
        Transform = Transform.Rotate(0, 0, 1, angle);
        UpdateTransform();
    }

    public void Scale(float scale)
    {
        Scale(scale, scale);
    }
    public void Scale(float scaleX, float scaleY)
    {
        Transform = Transform.Scale(scaleX, scaleY, 1);
        UpdateTransform();
    }

    public PointF TransformPoint(float x, float y)
    {
        var nVec = Vector4.Transform(new Vector4(x, y, 0, 1), Transform);
        return new(nVec.X, nVec.Y);
    }
    public PointF TransformPoint(PointF vec)
    {
        var nVec = Vector4.Transform(new Vector4(vec.X, vec.Y, 0, 1), Transform);
        return new(nVec.X, nVec.Y);
    }
    public PointF TransformPoint(netDxf.Vector2 vec)
    {
        var nVec = Vector4.Transform(new Vector4((float)vec.X, (float)vec.Y, 0, 1), Transform);
        return new(nVec.X, nVec.Y);
    }
    public PointF TransformPoint(netDxf.Vector3 vec)
    {
        var nVec = Vector4.Transform(new Vector4((float)vec.X, (float)vec.Y, (float)vec.Z, 1), Transform);
        return new(nVec.X, nVec.Y);
    }

    public PointF TransformVec(PointF vec)
    {
        var nVec = Vector4.Transform(new Vector4(vec.X, vec.Y, 0, 0), Transform);
        return new(nVec.X, nVec.Y);
    }
    public Vector2 TransformVec(Vector2 vec)
    {
        var nVec = Vector4.Transform(new Vector4(vec.X, vec.Y, 0, 0), Transform);
        return new(nVec.X, nVec.Y);
    }

    public float ScaleFactor()
    {
        return (TransformVec(new Vector2(1, 0))).Length();
    }

    public static PointF ConvertPoint(netDxf.Vector2 vec)
    {
        return new PointF((float)vec.X, (float)vec.Y);
    }

    public static PointF ConvertPoint(netDxf.Vector3 vec)
    {
        return new PointF((float)vec.X, (float)vec.Y);
    }
}

public class DrawData : TransformData
{
    private IGraphics _graphics;
    public IGraphics Graphics
    {
        get => _graphics;
        set
        {
            _graphics = value;
            _graphics.DrawData = this;
        }
    }

    private Stack<Color> _blockColours = new();

    public Color BlockColour { get; set; } = Color.Black;

    public void PushBlockColour(AciColor colour, EntityObject? obj)
    {
        _blockColours.Push(BlockColour);
        BlockColour = ResolveColour(colour, obj);
    }

    public void PushBlockColour(Color colour)
    {
        _blockColours.Push(BlockColour);
        BlockColour = colour;
    }

    public void PopBlockColour()
    {
        BlockColour = _blockColours.Pop();
    }

    public Color ResolveColour(AciColor colour, EntityObject? obj)
    {
        if (colour.IsByBlock)
        {
            return _blockColours.Count > 0
                ? BlockColour
                : (obj?.Layer is null ? Color.Black : ResolveColour(obj.Layer.Color, null));
        }
        else if (colour.IsByLayer)
        {
            return obj?.Layer is null ? Color.Black : ResolveColour(obj.Layer.Color, null);
        }
        if(colour.Index == 7)
        {
            return Color.Black;
        }
        else
        {
            return colour.ToColor();
        }
    }

    protected override void UpdateTransform()
    {
        base.UpdateTransform();
        Graphics.SetTransform(ProjectMatrix(Transform));
    }
}

public interface IGraphics
{
    DrawData DrawData { get; set; }

    void SetTransform(Matrix transform);

    void DrawLine(Color color, float thickness, params PointF[] points);

    void DrawPoint(Color color, float thickness, PointF point);

    void FillPolygon(Color color, params PointF[] points);

    /// <summary>
    /// Set the current font to be the default font at the given <paramref name="fontSize"/>.
    /// </summary>
    /// <param name="fontSize">Size of the new font.</param>
    void SetFont(float fontSize, FontStyle fontStyle = FontStyle.Regular);

    void SetFont(string fontName, float fontSize, FontStyle fontStyle = FontStyle.Regular);

    void DrawText(string text, Color color, PointF position, float rotation, TextFormat format);
    void DrawText(string text, Color color, PointF position, float rotation, float width, PointF scale);
    void DrawText(string text, Color color, PointF position, float rotation, SizeF size, PointF scale);

    void Clear(Color color);

    void Finish();
}

public class GdiGraphics : IGraphics
{
    public Graphics Graphics { get; set; }

    private Font Font { get; set; }

    public DrawData DrawData { get; set; }

    public GdiGraphics(Graphics graphics)
    {
        Graphics = graphics;
    }

    // public float ConvertThickness(float thickness)
    // {
    //     return thickness == 0 ? 1f / ScaleFactor() : thickness;
    // }

    public void DrawLine(Color color, float thickness, params PointF[] points)
    {
        if(points.Length == 2)
        {
            Graphics.DrawLine(new Pen(color, thickness), points[0], points[1]);
        }
        else
        {
            Graphics.DrawLines(new Pen(color, thickness), points);
        }
    }

    public void DrawPoint(Color color, float thickness, PointF point)
    {
        Graphics.FillEllipse(new SolidBrush(color), point.X - thickness / 2, point.Y - thickness / 2, point.X + thickness / 2, point.Y + thickness / 2);
    }

    private void TransformTextVertical(string text, Font font, PointF position, float rotation, TextFormat format)
    {
        DrawData.Translate(position);
        DrawData.Rotate(rotation);
        DrawData.Scale(1, -1);

        var ascentRatio = (float)font.FontFamily.GetCellAscent(font.Style) / font.FontFamily.GetLineSpacing(font.Style);

        var size = Graphics.MeasureString(text, font, new PointF(), StringFormat.GenericTypographic);

        var scaleFactor = ascentRatio * font.Height / size.Height;
        DrawData.Scale(scaleFactor, scaleFactor);

        switch (format.LineAlignment)
        {
            case TextLineAlignment.Center:
                DrawData.Translate(new PointF(0, -size.Height / 2));
                break;
            case TextLineAlignment.Bottom:
                DrawData.Translate(new PointF(0, -size.Height));
                break;
            case TextLineAlignment.Baseline:
                var baseline = ascentRatio * font.Height;
                DrawData.Translate(new PointF(0, -baseline));
                break;
            case TextLineAlignment.Top:
                baseline = ascentRatio * font.Height;
                var lineSpace = font.FontFamily.GetLineSpacing(font.Style);
                var ratio = font.GetHeight(Graphics) / lineSpace;
                DrawData.Translate(new PointF(0, -baseline + font.FontFamily.GetCellAscent(font.Style) * ratio));
                break;
        }
    }

    public void DrawText(string text, Color color, PointF position, float rotation, TextFormat format)
    {
        if (text.IsNullOrWhiteSpace())
        {
            return;
        }
        DrawData.PushTransform();
        TransformTextVertical(text, Font, position, rotation, format);

        var pos = 0;
        foreach(var line in text.Split(new char[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
        {
            DrawData.PushTransform();
            DrawData.Translate(0, pos * Font.Height);
            var size = Graphics.MeasureString(line, Font);

            switch (format.Alignment)
            {
                case TextAlignment.Left:
                    break;
                case TextAlignment.Center:
                    DrawData.Translate(new PointF(-(float)size.Width / 2, 0));
                    break;
                case TextAlignment.Right:
                    DrawData.Translate(new PointF(-(float)size.Width, 0));
                    break;
            }

            Graphics.DrawString(line, Font, new SolidBrush(color), new PointF(0, 0), StringFormat.GenericTypographic);
            DrawData.PopTransform();

            ++pos;
        }

        DrawData.PopTransform();
    }
    public void DrawText(string text, Color color, PointF position, float rotation, float width, PointF scale)
    {
        var size = Graphics.MeasureString(text, Font, new PointF(), StringFormat.GenericTypographic);
        DrawText(text, color, position, rotation, new SizeF(width, size.Height * width / size.Width), scale);
    }
    public void DrawText(string text, Color color, PointF position, float rotation, SizeF size, PointF scale)
    {
        DrawData.PushTransform();
        DrawData.Translate(position);
        DrawData.Rotate(rotation);
        DrawData.Scale(1, -1);

        var (width, height) = (size.Width, size.Height);

        DrawData.Translate(width / 2, height / 2);
        DrawData.Scale(scale.X, scale.Y);
        DrawData.Translate(-width / 2, -height / 2);

        DrawData.Scale(width / size.Width, height / size.Height);
        Graphics.DrawString(text, Font, new SolidBrush(color), new PointF(0, 0), StringFormat.GenericTypographic);

        DrawData.PopTransform();
    }

    public void FillPolygon(Color color, params PointF[] points)
    {
        Graphics.FillPolygon(new SolidBrush(color), points);
    }

    public void SetFont(string fontName, float fontSize, FontStyle fontStyle)
    {
        var fontFamily = new FontFamily(fontName);
        var font = new Font(fontFamily, fontSize, fontStyle);
        Font = font;
    }

    public void SetFont(float fontSize, FontStyle fontStyle)
    {
        var fontFamily = SystemFonts.DefaultFont.FontFamily;
        var font = new Font(fontFamily, fontSize, fontStyle);
        Font = font;
    }

    public void SetTransform(Matrix transform)
    {
        Graphics.Transform = transform;
    }

    public void Clear(Color color)
    {
        Graphics.Clear(color);
    }

    public void Finish()
    {
    }
}

public class PdfGraphics : IGraphics
{
    public DrawData DrawData { get; set; }

    private SPdfGraphics Graphics { get; set; }

    private Matrix _transform = new();

    private PdfFont _font = new PdfStandardFont(PdfFontFamily.Helvetica, 12, PdfFontStyle.Regular);
    private float _ascentRatio = 0;

    public PdfGraphics(SPdfGraphics graphics)
    {
        Graphics = graphics;
    }

    public void Clear(Color color)
    {
    }

    public void SetTransform(Matrix transform)
    {
        _transform = transform;
    }

    public void DrawLine(Color color, float thickness, params PointF[] points)
    {
        _transform.TransformPoints(points);

        var pen = new PdfPen(color, thickness);
        for(int i = 0; i < points.Length - 1; ++i)
        {
            Graphics.DrawLine(pen, points[i], points[i + 1]);
        }
    }

    public void DrawPoint(Color color, float thickness, PointF point)
    {
        var transformed = _transform.Transform(point);
        var brush = new PdfSolidBrush(color);
        Graphics.DrawEllipse(brush, transformed.X - thickness / 2, transformed.Y - thickness / 2, transformed.X + thickness / 2, transformed.Y + thickness / 2);
    }

    public void SetFont(float fontSize, FontStyle fontStyle = FontStyle.Regular)
    {
        var font = new Font(SystemFonts.DefaultFont.FontFamily, fontSize);
        _ascentRatio = (float)font.FontFamily.GetCellAscent(font.Style) / (float)font.FontFamily.GetLineSpacing(font.Style);
        _font = new PdfTrueTypeFont(font);
    }

    public void SetFont(string fontName, float fontSize, FontStyle fontStyle = FontStyle.Regular)
    {
        var font = new Font(fontName, fontSize);
        _ascentRatio = (float)font.FontFamily.GetCellAscent(font.Style) / (float)font.FontFamily.GetLineSpacing(font.Style);
        _font = new PdfTrueTypeFont(new Font(fontName, fontSize));
    }

    public void DrawText(string text, Color color, PointF position, float rotation, TextFormat format)
    {
        var fmt = new PdfStringFormat();
        fmt.Alignment = format.Alignment switch
        {
            TextAlignment.Center => PdfTextAlignment.Center,
            TextAlignment.Right => PdfTextAlignment.Right,
            TextAlignment.Justify => PdfTextAlignment.Justify,
            _ => PdfTextAlignment.Left,
        };
        fmt.LineAlignment = format.LineAlignment switch
        {
            TextLineAlignment.Center => PdfVerticalAlignment.Middle,
            TextLineAlignment.Bottom => PdfVerticalAlignment.Bottom,
            TextLineAlignment.Baseline => PdfVerticalAlignment.Bottom,
            _ => PdfVerticalAlignment.Top
        };
        if(format.LineAlignment == TextLineAlignment.Baseline)
        {
            fmt.EnableBaseline = true;
        }

        Graphics.Save();

        var point = _transform.Transform(position);
        Graphics.TranslateTransform(point.X, point.Y);
        Graphics.RotateTransform(_transform.GetRotation() - rotation);

        var scaled = _transform.GetScale();

        Graphics.ScaleTransform(scaled / _ascentRatio, scaled / _ascentRatio);
        Graphics.DrawString(text, _font, new PdfSolidBrush(color), new PointF(), fmt);

        Graphics.Restore();
    }
    public void DrawText(string text, Color color, PointF position, float rotation, float width, PointF scale)
    {
        Graphics.Save();

        var point = _transform.Transform(position);
        Graphics.TranslateTransform(point.X, point.Y);
        Graphics.RotateTransform(_transform.GetRotation() - rotation);
        var scaleFactor = (_font.Height / _font.Size) * _transform.GetScale();
        Graphics.ScaleTransform(scaleFactor, scaleFactor * scale.Y);
        if(scale.X != 1)
        {
            Graphics.TranslateTransform(width / 2, 0);
            Graphics.ScaleTransform(scale.X, 1);
            Graphics.TranslateTransform(-width / 2, 0);
        }

        Graphics.DrawString(text, _font, new PdfSolidBrush(color), new RectangleF(0, 0, width, float.MaxValue), new PdfStringFormat
        {
            Alignment = PdfTextAlignment.Justify,
            LineAlignment = PdfVerticalAlignment.Top,
            EnableBaseline = true
        });

        Graphics.Restore();
    }
    public void DrawText(string text, Color color, PointF position, float rotation, SizeF size, PointF scale)
    {
        Graphics.Save();

        var point = _transform.Transform(position);
        Graphics.TranslateTransform(point.X, point.Y);
        Graphics.RotateTransform(_transform.GetRotation() - rotation);
        var scaleFactor = (_font.Height / _font.Size) * _transform.GetScale();
        Graphics.ScaleTransform(scaleFactor, scaleFactor * scale.Y);
        if(scale.X != 1 || scale.Y != 1)
        {
            Graphics.TranslateTransform(size.Width / 2, size.Height / 2);
            Graphics.ScaleTransform(scale.X, 1);
            Graphics.TranslateTransform(-size.Width / 2, size.Height / 2);
        }

        Graphics.DrawString(text, _font, new PdfSolidBrush(color), new RectangleF(0, 0, size.Width, size.Height), new PdfStringFormat
        {
            Alignment = PdfTextAlignment.Justify,
            LineAlignment = PdfVerticalAlignment.Top,
            EnableBaseline = true
        });

        Graphics.Restore();
    }

    public void FillPolygon(Color color, params PointF[] points)
    {
        _transform.TransformPoints(points);
        var brush = new PdfSolidBrush(color);
        Graphics.DrawPolygon(brush, points);
    }

    public void Finish()
    {
    }
}

public class SvgGraphics : IGraphics
{
    public DrawData DrawData { get; set; }

    public SvgDocument Document { get; set; }

    private SvgMatrix _transform = new(new Matrix().Elements.ToList());

    private SvgGroup _group;
    private Matrix _groupTransform = new();

    private static string[][] _groupProps = new[]
    {
        new[] { "stroke-width", "style" },
        new[] { "vector-effect" }
    };

    public SvgGraphics(SvgDocument document)
    {
        Document = document;
        _group = new SvgGroup();
        Document.Stroke = new SvgColourServer(Color.Black);
    }

    private void PushGroup()
    {
        if(_group.Children.Count > 0)
        {
            Document.Children.Add(_group);
        }
    }

    private void AddChild(SvgElement item)
    {
        var transMat = _transform.Matrix;
        if(!Equals(transMat, _groupTransform))
        {
            PushGroup();

            _group = new();
            _group.Transforms = new() { _transform };
            _groupTransform = transMat;
        }
        _group.Children.Add(item);
    }

    public void SetTransform(Matrix transform)
    {
        _transform = new(transform.Elements.ToList());
    }

    public void DrawLine(Color color, float thickness, params PointF[] points)
    {
        if(points.Length == 2)
        {
            var line = new SvgLine
            {
                StartX = points[0].X,
                StartY = points[0].Y,
                EndX = points[1].X,
                EndY = points[1].Y
            };
            if(color != Color.Black)
            {
                line.Stroke = new SvgColourServer(color);
            }
            if(thickness == 0)
            {
                line.StrokeWidth = 1;
                line.CustomAttributes.Add("vector-effect", "non-scaling-stroke");
            }
            else
            {
                line.StrokeWidth = thickness;
            }
            AddChild(line);
        }
        else
        {
            var line = new SvgPolyline();
            if(color != Color.Black)
            {
                line.Stroke = new SvgColourServer(color);
            }
            foreach(var point in points)
            {
                line.Points.Add(point.X);
                line.Points.Add(point.Y);
            }
            if(thickness == 0)
            {
                line.StrokeWidth = 1;
                line.CustomAttributes.Add("vector-effect", "non-scaling-stroke");
            }
            else
            {
                line.StrokeWidth = thickness;
            }

            AddChild(line);
        }
    }

    public void DrawPoint(Color color, float thickness, PointF point)
    {
    }

    public void FillPolygon(Color color, params PointF[] points)
    {
    }

    public void SetFont(float fontSize, FontStyle fontStyle = FontStyle.Regular)
    {
    }

    public void SetFont(string fontName, float fontSize, FontStyle fontStyle = FontStyle.Regular)
    {
    }

    public void DrawText(string text, Color color, PointF position, float rotation, TextFormat format)
    {
    }

    public void Clear(Color color)
    {
    }

    public void Finish()
    {
        PushGroup();
    }

    public void DrawText(string text, Color color, PointF position, float rotation, float width, PointF scale)
    {
    }

    public void DrawText(string text, Color color, PointF position, float rotation, SizeF size, PointF scale)
    {
    }
}
