using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Linq;

namespace Svg
{
    [SvgElement("linearGradient")]
    public sealed class SvgLinearGradientServer : SvgGradientServer
    {
        [SvgAttribute("x1")]
        public SvgUnit X1
        {
            get
            {
                return this.Attributes.GetAttribute<SvgUnit>("x1");
            }
            set
            {
                Attributes["x1"] = value;
            }
        }

        [SvgAttribute("y1")]
        public SvgUnit Y1
        {
            get
            {
                return this.Attributes.GetAttribute<SvgUnit>("y1");
            }
            set
            {
                this.Attributes["y1"] = value;
            }
        }

        [SvgAttribute("x2")]
        public SvgUnit X2
        {
            get
            {
                return this.Attributes.GetAttribute<SvgUnit>("x2");
            }
            set
            {
                Attributes["x2"] = value;
            }
        }

        [SvgAttribute("y2")]
        public SvgUnit Y2
        {
            get
            {
                return this.Attributes.GetAttribute<SvgUnit>("y2");
            }
            set
            {
                this.Attributes["y2"] = value;
            }
        }

        private bool IsInvalid
        {
            get
            {
                // Need at least 2 colours to do the gradient fill
                return this.Stops.Count < 2;
            }
        }

        public SvgLinearGradientServer()
        {
            X1 = new SvgUnit(SvgUnitType.Percentage, 0F);
            Y1 = new SvgUnit(SvgUnitType.Percentage, 0F);
            X2 = new SvgUnit(SvgUnitType.Percentage, 100F);
            Y2 = new SvgUnit(SvgUnitType.Percentage, 0F);
        }

        public override Brush GetBrush(SvgVisualElement renderingElement, ISvgRenderer renderer, float opacity)
        {
            LoadStops(renderingElement);
            if (IsInvalid)
            {
                return null;
            }

            try
            {
                if (this.GradientUnits == SvgCoordinateUnits.ObjectBoundingBox) renderer.SetBoundable(renderingElement);

                var specifiedStart = CalculateStart(renderer);
                var specifiedEnd = CalculateEnd(renderer);

                var effectiveStart = specifiedStart;
                var effectiveEnd = specifiedEnd;

                if (NeedToExpandGradient(renderingElement, specifiedStart, specifiedEnd))
                {
                    var expansion = ExpandGradient(renderingElement, specifiedStart, specifiedEnd);
                    effectiveStart = expansion.StartPoint;
                    effectiveEnd = expansion.EndPoint;
                }

                return new LinearGradientBrush(effectiveStart, effectiveEnd, System.Drawing.Color.Transparent, System.Drawing.Color.Transparent)
                {
                    InterpolationColors = CalculateColorBlend(renderer, opacity, specifiedStart, effectiveStart, specifiedEnd, effectiveEnd),
                    WrapMode = WrapMode.TileFlipX
                };
            }
            finally
            {
                if (this.GradientUnits == SvgCoordinateUnits.ObjectBoundingBox) renderer.PopBoundable();
            }
        }

        private PointF CalculateStart(ISvgRenderer renderer)
        {
            return TransformPoint(SvgUnit.GetDevicePointOffset(this.X1, this.Y1, renderer, this));
        }

        private PointF CalculateEnd(ISvgRenderer renderer)
        {
            return TransformPoint(SvgUnit.GetDevicePointOffset(this.X2, this.Y2, renderer, this));
        }

        private bool NeedToExpandGradient(ISvgBoundable boundable, PointF specifiedStart, PointF specifiedEnd)
        {
            return SpreadMethod == SvgGradientSpreadMethod.Pad && (boundable.Bounds.Contains(specifiedStart) || boundable.Bounds.Contains(specifiedEnd));
        }

        public struct GradientPoints
        {
            public PointF StartPoint;
            public PointF EndPoint;

            public GradientPoints(PointF startPoint, PointF endPoint)
            {
                this.StartPoint = startPoint;
                this.EndPoint = endPoint;
            }
        }

        private GradientPoints ExpandGradient(ISvgBoundable boundable, PointF specifiedStart, PointF specifiedEnd)
        {
            if (!NeedToExpandGradient(boundable, specifiedStart, specifiedEnd))
            {
                Debug.Fail("Unexpectedly expanding gradient when not needed!");
                return new GradientPoints(specifiedStart, specifiedEnd);
            }

            var specifiedLength = CalculateDistance(specifiedStart, specifiedEnd);
            var specifiedUnitVector = new PointF((specifiedEnd.X - specifiedStart.X) / (float)specifiedLength, (specifiedEnd.Y - specifiedStart.Y) / (float)specifiedLength);

            var effectiveStart = specifiedStart;
            var effectiveEnd = specifiedEnd;

            var elementDiagonal = (float)CalculateDistance(new PointF(boundable.Bounds.Left, boundable.Bounds.Top), new PointF(boundable.Bounds.Right, boundable.Bounds.Bottom));

            var expandedStart = MovePointAlongVector(effectiveStart, specifiedUnitVector, -elementDiagonal);
            var expandedEnd = MovePointAlongVector(effectiveEnd, specifiedUnitVector, elementDiagonal);

            var intersectionPoints = new LineF(expandedStart.X, expandedStart.Y, expandedEnd.X, expandedEnd.Y).Intersection(boundable.Bounds);

            if (boundable.Bounds.Contains(specifiedStart))
            {
                effectiveStart = CalculateClosestIntersectionPoint(expandedStart, intersectionPoints);

                effectiveStart = MovePointAlongVector(effectiveStart, specifiedUnitVector, -1);
            }

            if (boundable.Bounds.Contains(specifiedEnd))
            {
                effectiveEnd = CalculateClosestIntersectionPoint(expandedEnd, intersectionPoints);

                effectiveEnd = MovePointAlongVector(effectiveEnd, specifiedUnitVector, 1);
            }

            return new GradientPoints(effectiveStart, effectiveEnd);
        }

        private ColorBlend CalculateColorBlend(ISvgRenderer renderer, float opacity, PointF specifiedStart, PointF effectiveStart, PointF specifiedEnd, PointF effectiveEnd)
        {
            var colorBlend = GetColorBlend(renderer, opacity, false);

            var startDelta = CalculateDistance(specifiedStart, effectiveStart);
            var endDelta = CalculateDistance(specifiedEnd, effectiveEnd);

            if (!(startDelta > 0) && !(endDelta > 0))
            {
                return colorBlend;
            }

            var specifiedLength = CalculateDistance(specifiedStart, specifiedEnd);
            var specifiedUnitVector = new PointF((specifiedEnd.X - specifiedStart.X) / (float)specifiedLength, (specifiedEnd.Y - specifiedStart.Y) / (float)specifiedLength);

            var effectiveLength = CalculateDistance(effectiveStart, effectiveEnd);

            for (var i = 0; i < colorBlend.Positions.Length; i++)
            {
                var originalPoint = MovePointAlongVector(specifiedStart, specifiedUnitVector, (float)specifiedLength * colorBlend.Positions[i]);

                var distanceFromEffectiveStart = CalculateDistance(effectiveStart, originalPoint);

                colorBlend.Positions[i] = (float)Math.Round(Math.Max(0F, Math.Min((distanceFromEffectiveStart / effectiveLength), 1.0F)), 5);
            }

            if (startDelta > 0)
            {
                colorBlend.Positions = new[] { 0F }.Concat(colorBlend.Positions).ToArray();
                colorBlend.Colors = new[] { colorBlend.Colors.First() }.Concat(colorBlend.Colors).ToArray();
            }

            if (endDelta > 0)
            {
                colorBlend.Positions = colorBlend.Positions.Concat(new[] { 1F }).ToArray();
                colorBlend.Colors = colorBlend.Colors.Concat(new[] { colorBlend.Colors.Last() }).ToArray();
            }

            return colorBlend;
        }

        private static PointF CalculateClosestIntersectionPoint(PointF sourcePoint, IList<PointF> targetPoints)
        {
            Debug.Assert(targetPoints.Count == 2, "Unexpected number of intersection points!");

            return CalculateDistance(sourcePoint, targetPoints[0]) < CalculateDistance(sourcePoint, targetPoints[1]) ? targetPoints[0] : targetPoints[1];
        }

        private static PointF MovePointAlongVector(PointF start, PointF unitVector, float distance)
        {
            return start + new SizeF(unitVector.X * distance, unitVector.Y * distance);
        }

        public override SvgElement DeepCopy()
        {
            return DeepCopy<SvgLinearGradientServer>();
        }

        public override SvgElement DeepCopy<T>()
        {
            var newObj = base.DeepCopy<T>() as SvgLinearGradientServer;
            newObj.X1 = this.X1;
            newObj.Y1 = this.Y1;
            newObj.X2 = this.X2;
            newObj.Y2 = this.Y2;
            return newObj;

        }

        private sealed class LineF
        {
            private float X1
            {
                get;
                set;
            }

            private float Y1
            {
                get;
                set;
            }

            private float X2
            {
                get;
                set;
            }

            private float Y2
            {
                get;
                set;
            }

            public LineF(float x1, float y1, float x2, float y2)
            {
                X1 = x1;
                Y1 = y1;
                X2 = x2;
                Y2 = y2;
            }

            public List<PointF> Intersection(RectangleF rectangle)
            {
                var result = new List<PointF>();

                AddIfIntersect(this, new LineF(rectangle.X, rectangle.Y, rectangle.Right, rectangle.Y), result);
                AddIfIntersect(this, new LineF(rectangle.Right, rectangle.Y, rectangle.Right, rectangle.Bottom), result);
                AddIfIntersect(this, new LineF(rectangle.Right, rectangle.Bottom, rectangle.X, rectangle.Bottom), result);
                AddIfIntersect(this, new LineF(rectangle.X, rectangle.Bottom, rectangle.X, rectangle.Y), result);

                return result;
            }

            /// <remarks>http://community.topcoder.com/tc?module=Static&d1=tutorials&d2=geometry2</remarks>
            private PointF? Intersection(LineF other)
            {
                const int precision = 8;

                var a1 = (double)Y2 - Y1;
                var b1 = (double)X1 - X2;
                var c1 = a1 * X1 + b1 * Y1;

                var a2 = (double)other.Y2 - other.Y1;
                var b2 = (double)other.X1 - other.X2;
                var c2 = a2 * other.X1 + b2 * other.Y1;

                var det = a1 * b2 - a2 * b1;
                if (det == 0)
                {
                    return null;
                }
                else
                {
                    var xi = (b2 * c1 - b1 * c2) / det;
                    var yi = (a1 * c2 - a2 * c1) / det;

                    if (Math.Round(Math.Min(X1, X2), precision) <= Math.Round(xi, precision) &&
                        Math.Round(xi, precision) <= Math.Round(Math.Max(X1, X2), precision) &&
                        Math.Round(Math.Min(Y1, Y2), precision) <= Math.Round(yi, precision) &&
                        Math.Round(yi, precision) <= Math.Round(Math.Max(Y1, Y2), precision) &&
                        Math.Round(Math.Min(other.X1, other.X2), precision) <= Math.Round(xi, precision) &&
                        Math.Round(xi, precision) <= Math.Round(Math.Max(other.X1, other.X2), precision) &&
                        Math.Round(Math.Min(other.Y1, other.Y2), precision) <= Math.Round(yi, precision) &&
                        Math.Round(yi, precision) <= Math.Round(Math.Max(other.Y1, other.Y2), precision))
                    {
                        return new PointF((float)xi, (float)yi);
                    }
                    else
                    {
                        return null;
                    }
                }


                //var a1 = Y2 - Y1;
                //var b1 = X1 - X2;
                //var c1 = X2 * Y1 - X1 * Y2;

                //var r3 = a1 * other.X1 + b1 * other.Y1 + c1;
                //var r4 = a1 * other.X2 + b1 * other.Y2 + c1;

                //if (r3 != 0 && r4 != 0 && Math.Sign(r3) == Math.Sign(r4))
                //{
                //    return null;
                //}

                //var a2 = other.Y2 - other.Y1;
                //var b2 = other.X1 - other.X2;
                //var c2 = other.X2 * other.Y1 - other.X1 * other.Y2;

                //var r1 = a2 * X1 + b2 * Y1 + c2;
                //var r2 = a2 * X2 + b2 * Y2 + c2;

                //if (r1 != 0 && r2 != 0 && Math.Sign(r1) == Math.Sign(r2))
                //{
                //    return (null);
                //}

                //var denom = a1 * b2 - a2 * b1;

                //if (denom == 0)
                //{
                //    return null;
                //}

                //var offset = denom < 0 ? -denom / 2 : denom / 2;

                //var num = b1 * c2 - b2 * c1;
                //var x = (num < 0 ? num - offset : num + offset) / denom;

                //num = a2 * c1 - a1 * c2;
                //var y = (num < 0 ? num - offset : num + offset) / denom;

                //return new PointF(x, y);
            }

            private static void AddIfIntersect(LineF first, LineF second, ICollection<PointF> result)
            {
                var intersection = first.Intersection(second);

                if (intersection != null)
                {
                    result.Add(intersection.Value);
                }
            }
        }
    }
}