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

namespace PCUT.Extensions
{
    public static class SvgExtensions
    {
        private static readonly PathConverter _pathConverter = PathConverter.Builder()
                                                                            .AddPath()
                                                                            .AddRect()
                                                                            .AddPolygon()
                                                                            .AddCircle()
                                                                            .AddEllipse().Build();

        public static XDocument PreProcessSvg(this XDocument document, out double width, out double height)
        {
            var data = document.ScaleToUnit();
            width = data.Width;
            height = data.Height;
            var queue = new Queue<(XElement, SvgTransform)>();
            queue.Enqueue((document.Root, data.ScaleTransform));
            ConvertToPath(queue);
            return document.ApplyCssStyle(data.ScaleTransform).SetAutoSize();
        }

        private static void ConvertToPath(Queue<(XElement Element, SvgTransform RootTransform)> queue)
        {
            while (queue.TryDequeue(out var item))
            {
                var element = item.Element;
                var rootTransform = item.RootTransform;

                var svgPath = _pathConverter.GetSvgPath(element);
                if (svgPath != null)
                {
                    var transform = element.GetTransform(rootTransform);
                    transform.SetNext(rootTransform);

                    var newElement = GetTransformedPath(svgPath, transform);

                    _pathConverter.CopyAttribute(element, newElement);

                    newElement.SetAttributeValue("id", $"P_{Guid.NewGuid():N}");
                    element.ReplaceWith(newElement);
                }

                if (element.Name.LocalName == "g" || element.Name.LocalName == "svg")
                {
                    var transform = element.GetTransform(rootTransform);
                    transform.SetNext(rootTransform);
                    element.RemoveAttribute("transform");
                    if (element.Name.LocalName == "g")
                        element.SetAttributeValue("id", $"G_{Guid.NewGuid():N}");

                    foreach (var childElement in element.Elements())
                        queue.Enqueue((childElement, transform));
                }
            }
        }

        private static XElement GetTransformedPath(SvgPath path, SvgTransform transform)
        {
            var builder = new StringBuilder();
            path = transform.Apply(path);
            foreach (var segment in path.Segments)
            {
                switch (segment)
                {
                    case MoveCommand moveCommand:
                        {
                            builder.Append("M ")
                                .Append(moveCommand.X).Append(',').Append(moveCommand.Y)
                                .Append(' ');
                            break;
                        }
                    case LineCommand lineCommand:
                        {
                            if (lineCommand.ClosePath)
                                builder.Append('Z');
                            else
                                builder.Append("L ")
                                    .Append(lineCommand.ToX).Append(',').Append(lineCommand.ToY)
                                    .Append(' ');
                            break;
                        }
                    case BezierCommand curveCommand:
                        {
                            builder.Append(curveCommand.IsQuadratic ? "Q " : "C ")
                                .Append(curveCommand.Cp1.X).Append(',').Append(curveCommand.Cp1.Y)
                                .Append(' ')
                                .Append(curveCommand.Cp2OrEnd.X).Append(',').Append(curveCommand.Cp2OrEnd.Y)
                                .Append(' ');
                            if (!curveCommand.IsQuadratic)
                                builder.Append(curveCommand.End.X).Append(',').Append(curveCommand.End.Y)
                                    .Append(' ');
                            break;
                        }
                    default:
                        {
                            break;
                        }
                }
            }
            var transformedPath = new XElement(XName.Get("path", "http://www.w3.org/2000/svg"));
            transformedPath.SetAttributeValue("d", builder.ToString());
            return transformedPath;
        }

        public static SvgTransform GetTransform(this XElement element, SvgTransform rootTransform = null)
        {
            if (rootTransform == null)
                rootTransform = SvgTransform.Default;

            var transforms = new List<(int Index, SvgTransform Transform)>();

            var transformAttribute = element.Attribute("transform")?.Value;
            if (transformAttribute != null)
            {
                foreach (var (i, translateData) in transformAttribute.GetTransform("translate", 2))
                {
                    var dx = double.Parse(translateData[0]);
                    var dy = translateData.Length > 1 ? double.Parse(translateData[1]) : 0;
                    var translate = new TranslateTransform(dx, dy);
                    transforms.Add((i, translate));
                }

                foreach (var (i, rotateData) in transformAttribute.GetTransform("rotate", 3))
                {
                    var degree = double.Parse(rotateData[0]);
                    var originX = rotateData.Length > 2 ? double.Parse(rotateData[1]) : 0;
                    var originY = rotateData.Length > 2 ? double.Parse(rotateData[2]) : 0;
                    var rotate = new RotateTransform(degree, originX, originY);
                    transforms.Add((i, rotate));
                }

                foreach (var (i, scaleData) in transformAttribute.GetTransform("scale", 2))
                {
                    var scaleX = double.Parse(scaleData[0]);
                    var scaleY = scaleData.Length > 1 ? double.Parse(scaleData[1]) : scaleX;
                    var scale = new ScaleTransform(scaleX, scaleY);
                    transforms.Add((i, scale));
                }

                foreach (var (i, scaleData) in transformAttribute.GetTransform("matrix", 6))
                {
                    var a = double.Parse(scaleData[0]);
                    var b = double.Parse(scaleData[1]);
                    var c = double.Parse(scaleData[2]);
                    var d = double.Parse(scaleData[3]);
                    var e = double.Parse(scaleData[4]);
                    var f = double.Parse(scaleData[5]);
                    var matrix = new MatrixTransform(a, b, c, d, e, f);
                    transforms.Add((i, matrix));
                }
            }

            SvgTransform transform;
            if (!transforms.Any())
            {
                transform = rootTransform;
            }
            else
            {
                transforms.Sort((x, y) => x.Index.CompareTo(y.Index));
                transform = transforms.First().Transform;
                transform.SetNext(rootTransform);

                foreach (var item in transforms.Skip(1))
                {
                    item.Transform.SetNext(transform);
                    transform = item.Transform;
                }
            }
            return transform;
        }

        private static IEnumerable<(int Index, string[] Data)> GetTransform(this string transformAttribute, string key, int maxLength)
        {
            var index = transformAttribute.IndexOf(key);
            while (index != -1)
            {
                var s = transformAttribute.IndexOf('(', index);
                var e = transformAttribute.IndexOf(')', index);
                yield return (index, transformAttribute.Substring(s + 1, e - s - 1).Split(new[] { ' ', ',' }, maxLength, StringSplitOptions.RemoveEmptyEntries));
                index = transformAttribute.IndexOf(key, e);
            }
            yield break;
        }

        public static (double Width, double Height, SvgTransform ScaleTransform) ScaleToUnit(this XDocument document)
        {
            var width = ConvertToUnit(document, "width");
            var height = ConvertToUnit(document, "height");
            var (scaleX, scaleY) = ScaleViewBox(document, width, height);

            SvgTransform scaleTransform;
            if (scaleX == 1 && scaleY == 1)
                scaleTransform = SvgTransform.Default;
            else
                scaleTransform = new ScaleTransform(scaleX, scaleY);
            return (width, height, scaleTransform);
        }

        private static double ConvertToUnit(XDocument document, string dimension)
        {
            var dimensionAttribute = document.Root.Attribute(dimension);
            var dimensionValue = dimensionAttribute.Value;
            var match = Regex.Match(dimensionValue, "^(?<value>[\\d]+(?:[\\.,][\\d]*)?)(?<unit>mm|cm|in|pt|pc|px)?$");
            double scale;
            switch (match.Groups["unit"].Value)
            {
                case "in": { scale = 25.4; break; }
                case "cm": { scale = 10; break; }
                case "pc": { scale = 25.4 / 16; break; }
                case "pt": { scale = 25.4 / 72; break; }
                case "px": { scale = 25.4 / 96; break; }
                default: { scale = 1; break; }
            }
            var value = double.Parse(match.Groups["value"].Value) * scale;
            document.Root.SetAttributeValue(dimension, value);
            return value;
        }

        private static (double ScaleX, double ScaleY) ScaleViewBox(XDocument document, double width, double height)
        {
            var viewBoxAttribute = document.Root.Attribute("viewBox");
            var viewBox = viewBoxAttribute.Value;
            var viewBoxData = viewBox.Split(' ', 4);
            var minX = double.Parse(viewBoxData[0]);
            var minY = double.Parse(viewBoxData[1]);
            var viewWidth = double.Parse(viewBoxData[2]);
            var viewHeight = double.Parse(viewBoxData[3]);
            document.Root.SetAttributeValue("viewBox", $"{minX} {minY} {width} {height}");
            return (width / viewWidth, height / viewHeight);
        }

        public static XDocument ApplyCssStyle(this XDocument doc, SvgTransform rootTransform)
        {
            var styles = doc.GetCssStyleDefinitions();
            var scale = rootTransform is ScaleTransform scaleTransform ? scaleTransform.ScaleX : 1d;
            if (styles.Any() && scale != 1d)
            {
                foreach (var key in styles.Keys.ToArray())
                {
                    styles[key] = Regex.Replace(styles[key], "stroke-width:(?<value>[\\d]+(?:[\\.,][\\d]*)?)", match => $"stroke-width:{double.Parse(match.Groups["value"].Value) * scale}");
                }
            }
            foreach (var element in doc.Descendants(XName.Get("path", "http://www.w3.org/2000/svg")))
            {
                var classes = element.Attribute("class")?.Value.Split(' ', StringSplitOptions.RemoveEmptyEntries).Select(x => $".{x}");
                if (classes != null)
                {
                    var style = string.Join(';', classes.Select(@class => styles.TryGetValue(@class, out var value) ? value : null).Where(x => x != null));
                    element.SetAttributeValue("style", style);
                }
                else
                {
                    var styleAttribute = element.Attribute("style");
                    if (styleAttribute != null)
                    {
                        var style = Regex.Replace(styleAttribute.Value, "stroke-width:(?<value>[\\d]+(?:[\\.,][\\d]*)?)", match => $"stroke-width:{double.Parse(match.Groups["value"].Value) * scale}");
                        styleAttribute.SetValue(style);
                    }
                }
            }
            return doc;
        }

        public static IDictionary<string, string> GetCssStyleDefinitions(this XDocument doc)
        {
            var cdata = doc.DescendantNodes()
                           .OfType<XCData>()
                           .Where(x => x.Parent.Name.LocalName == "style" && x.Parent.Attribute("type")?.Value == "text/css")
                           .FirstOrDefault();
            if (cdata == null)
                return new Dictionary<string, string>();

            var styles = cdata.Value.Replace("\n", "")
                                    .Split(' ', StringSplitOptions.RemoveEmptyEntries)
                                    .Where(x => !string.IsNullOrEmpty(x))
                                    .Select((x, i) => (Value: x, Index: i / 2))
                                    .GroupBy(x => x.Index, x => x.Value)
                                    .ToDictionary(x => x.ElementAt(0), x => x.ElementAt(1).Trim('{', '}'));
            cdata.Remove();
            return styles;
        }
    }
}
