using PCUT.Extensions.Transforms;
using SvgPathProperties;
using SvgPathProperties.Base;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;

namespace PCUT.Extensions
{
    public static class HpglExtensions
    {
        private const double MinimumCurveSegmentLength = 1; // mm
        private const double UnitLength = 0.025; // mm
        private const int BufferLength = 15000; // byte

        class StartPointComparer : IComparer<Point>
        {
            public static StartPointComparer Instance = new StartPointComparer();
            public int Compare(Point x, Point y)
            {
                return x.X != y.X ? x.X.CompareTo(y.X) : y.Y.CompareTo(x.Y);
            }
        }

        public static IEnumerable<(string Path, TimeSpan EstimateTime)> ToHPGL(
            this XDocument svg,
            (double Width, double Height) size,
            int speed, // mm/s
            int pen = 0)
        {
            var minCurveSegmentLength = MinimumCurveSegmentLength / UnitLength;

            var widthUnit = (int)Math.Ceiling(size.Width / UnitLength);
            var heightUnit = (int)Math.Ceiling(size.Height / UnitLength);

            SvgTransform transform = new TranslateTransform(0, heightUnit);

            var flipTransform = new ScaleTransform(1, -1);
            flipTransform.SetNext(transform);
            transform = flipTransform;

            var scaleTransform = new ScaleTransform(1 / UnitLength, 1 / UnitLength);
            scaleTransform.SetNext(transform);
            transform = scaleTransform;
            
            var paths = new List<SvgPath>();
            svg.GetPaths(paths, transform);

            yield return ($"IN;SP{pen};", TimeSpan.FromMilliseconds(200));
            var builder = new StringBuilder();
            TimeSpan timeSpan = TimeSpan.Zero;
            var maxX = 0d;
            var length = 0d;
            foreach (var data in paths.OrderBy(x => x.GetPointAtLength(0), StartPointComparer.Instance))
            {
                foreach (var segment in data.Segments)
                {
                    switch (segment)
                    {
                        case MoveCommand moveCommand:
                            {
                                var lengthX = Math.Abs(maxX - moveCommand.X);
                                builder.AddHpglMove(moveCommand.X, moveCommand.Y, ref maxX);
                                length += (lengthX + heightUnit) * 0.75;
                                break;
                            }
                        case LineCommand lineCommand:
                            {
                                builder.AddHpglLine(lineCommand.ToX, lineCommand.ToY, ref maxX);
                                length += lineCommand.Length;
                                break;
                            }
                        case BezierCommand bezierCommand:
                            {
                                var iterationCount = (int)Math.Ceiling(bezierCommand.Length / minCurveSegmentLength);
                                for (int i = 1; i < iterationCount; i++)
                                {
                                    var point = bezierCommand.GetPointAtLength(i * minCurveSegmentLength);
                                    builder.AddHpglLine(point.X, point.Y, ref maxX);
                                    length += minCurveSegmentLength;
                                    if (builder.Length > BufferLength)
                                    {
                                        timeSpan = CalculateWaitTime(length, speed);
                                        yield return (builder.ToString(), timeSpan);
                                        builder.Clear();
                                        length = 0;
                                    }
                                }
                                var end = bezierCommand.IsQuadratic ? bezierCommand.Cp2OrEnd : bezierCommand.End;
                                builder.AddHpglLine(end.X, end.Y, ref maxX);
                                length += minCurveSegmentLength;
                                break;
                            }
                        case ArcCommand arcCommand:
                            {
                                var iterationCount = (int)Math.Ceiling(arcCommand.Length / minCurveSegmentLength);
                                for (int i = 1; i < iterationCount; i++)
                                {
                                    var point = arcCommand.GetPointAtLength(i * minCurveSegmentLength);
                                    builder.AddHpglLine(point.X, point.Y, ref maxX);
                                    length += minCurveSegmentLength;
                                    if (builder.Length > BufferLength)
                                    {
                                        timeSpan = CalculateWaitTime(length, speed);
                                        yield return (builder.ToString(), timeSpan);
                                        builder.Clear();
                                        length = 0;
                                    }
                                }
                                builder.AddHpglLine(arcCommand.ToX, arcCommand.ToY, ref maxX);
                                length += minCurveSegmentLength;
                                break;
                            }
                    }
                    if (builder.Length > BufferLength)
                    {
                        timeSpan = CalculateWaitTime(length, speed);
                        yield return (builder.ToString(), timeSpan);
                        builder.Clear();
                        length = 0;
                    }
                }
                timeSpan = CalculateWaitTime(length, speed);
                yield return (builder.ToString(), timeSpan);
                builder.Clear();
                length = 0;
            }
            yield return ($"PU{maxX + 2000},0;IN;", TimeSpan.Zero);
        }

        private static void GetPaths(this XDocument document, List<SvgPath> paths, SvgTransform transform = null)
        {
            foreach (var component in document.Root.Descendants().Where(z => z.Name.LocalName == "path"))
            {
                if (!component.IsHidden())
                {
                    var path = transform.Apply(new SvgPath(component.Attribute("d").Value, unarc: true));
                    paths.Add(path);
                }
            }
        }

        private static TimeSpan CalculateWaitTime(double length, int speed)
        {
            return TimeSpan.FromSeconds((int)Math.Ceiling((length * UnitLength) / speed));
        }

        private static StringBuilder AddHpglMove(this StringBuilder builder, double x, double y, ref double maxX)
        {
            x = Math.Round(x);
            y = Math.Round(y);
            maxX = Math.Max(x, maxX);
            return builder
                .Append("PU")
                .Append(x).Append(',')
                .Append(y).Append(";");
        }

        private static StringBuilder AddHpglLine(this StringBuilder builder, double x, double y, ref double maxX)
        {
            x = Math.Round(x);
            y = Math.Round(y);
            maxX = Math.Max(x, maxX);
            return builder
                .Append("PD")
                .Append(x).Append(',')
                .Append(y).Append(";");
        }
    }
}
