using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

namespace Unity.VisualScripting
{
    public abstract class StateTransitionWidget<TStateTransition> : NodeWidget<StateCanvas, TStateTransition>, IStateTransitionWidget
        where TStateTransition : class, IStateTransition
    {
        protected StateTransitionWidget(StateCanvas canvas, TStateTransition transition) : base(canvas, transition) { }


        #region Model

        protected TStateTransition transition => element;

        protected IStateTransitionDebugData transitionDebugData => GetDebugData<IStateTransitionDebugData>();

        private StateTransitionDescription description;

        private StateTransitionAnalysis analysis => transition.Analysis<StateTransitionAnalysis>(context);

        protected override void CacheDescription()
        {
            description = transition.Description<StateTransitionDescription>();

            label.text = description.label;
            label.image = description.icon?[IconSize.Small];
            label.tooltip = description.tooltip;

            if (!revealLabel)
            {
                label.tooltip = label.text + ": " + label.tooltip;
            }

            Reposition();
        }

        #endregion

        #region Lifecycle

        public override void BeforeFrame()
        {
            base.BeforeFrame();

            if (showDroplets)
            {
                GraphGUI.UpdateDroplets(canvas, droplets, transitionDebugData.lastBranchFrame, ref lastBranchTime, ref dropTime);
            }

            if (currentInnerWidth != targetInnerWidth)
            {
                Reposition();
            }
        }

        #endregion


        #region Contents

        private GUIContent label { get; } = new GUIContent();

        #endregion


        #region Positioning

        private readonly List<IStateTransition> siblingStateTransitions = new List<IStateTransition>();

        private Rect sourcePosition;

        private Rect destinationPosition;

        private Edge sourceEdge;

        private Edge entryEdge;

        private Edge exitEdge;

        private Edge destinationEdge;

        private Vector2 sourceEdgeCenter;

        private Vector2 entryEdgeCenter;

        private Vector2 exitEdgeCenter;

        private Vector2 destinationEdgeCenter;

        private Vector2 middle;

        private Rect _position;

        private Rect _clippingPosition;

        private float targetInnerWidth;

        private float currentInnerWidth;

        private bool revealInitialized;

        private float minBend
        {
            get
            {
                if (transition.source != transition.destination)
                {
                    return 15;
                }
                else
                {
                    return (middle.y - canvas.Widget(transition.source).position.center.y) / 2;
                }
            }
        }

        private float relativeBend => 1 / 4f;

        Edge IStateTransitionWidget.sourceEdge => sourceEdge;

        public override IEnumerable<IWidget> positionDependencies
        {
            get
            {
                yield return canvas.Widget(transition.source);
                yield return canvas.Widget(transition.destination);
            }
        }

        public override IEnumerable<IWidget> positionDependers
        {
            get
            {
                // Return all sibling transitions. This is an asymetrical dependency / depender
                // relation (because the siblings are not included in the dependers) to force
                // repositioning of siblings while avoiding stack overflow.

                foreach (var graphTransition in canvas.graph.transitions)
                {
                    var current = transition == graphTransition;

                    var analog =
                        transition.source == graphTransition.source &&
                        transition.destination == graphTransition.destination;

                    var inverted =
                        transition.source == graphTransition.destination &&
                        transition.destination == graphTransition.source;

                    if (!current && (analog || inverted))
                    {
                        var widget = canvas.Widget(graphTransition);

                        if (widget.isPositionValid) // Avoid stack overflow
                        {
                            yield return widget;
                        }
                    }
                }
            }
        }

        public Rect iconPosition { get; private set; }

        public Rect clipPosition { get; private set; }

        public Rect labelInnerPosition { get; private set; }

        public override Rect position
        {
            get { return _position; }
            set { }
        }

        public override Rect clippingPosition => _clippingPosition;

        public override void CachePositionFirstPass()
        {
            // Calculate the size immediately, because other transitions will rely on it for positioning

            targetInnerWidth = Styles.eventIcon.fixedWidth;

            var labelWidth = Styles.label.CalcSize(label).x;
            var labelHeight = EditorGUIUtility.singleLineHeight;

            if (revealLabel)
            {
                targetInnerWidth += Styles.spaceAroundIcon;
                targetInnerWidth += labelWidth;
            }

            if (!revealInitialized)
            {
                currentInnerWidth = targetInnerWidth;
                revealInitialized = true;
            }

            currentInnerWidth = Mathf.Lerp(currentInnerWidth, targetInnerWidth, canvas.repaintDeltaTime * Styles.revealSpeed);

            if (Mathf.Abs(targetInnerWidth - currentInnerWidth) < 1)
            {
                currentInnerWidth = targetInnerWidth;
            }

            var innerWidth = currentInnerWidth;
            var innerHeight = labelHeight;

            var edgeSize = InnerToEdgePosition(new Rect(0, 0, innerWidth, innerHeight)).size;
            var edgeWidth = edgeSize.x;
            var edgeHeight = edgeSize.y;

            _position.width = edgeWidth;
            _position.height = edgeHeight;
        }

        public override void CachePosition()
        {
            var innerWidth = innerPosition.width;
            var innerHeight = innerPosition.height;
            var edgeWidth = edgePosition.width;
            var edgeHeight = edgePosition.height;
            var labelWidth = Styles.label.CalcSize(label).x;
            var labelHeight = EditorGUIUtility.singleLineHeight;

            sourcePosition = canvas.Widget(transition.source).position;
            destinationPosition = canvas.Widget(transition.destination).position;

            Vector2 sourceClosestPoint;
            Vector2 destinationClosestPoint;
            LudiqGUIUtility.ClosestPoints(sourcePosition, destinationPosition, out sourceClosestPoint, out destinationClosestPoint);

            if (transition.destination != transition.source)
            {
                GraphGUI.GetConnectionEdge
                    (
                        sourceClosestPoint,
                        destinationClosestPoint,
                        out sourceEdge,
                        out destinationEdge
                    );
            }
            else
            {
                sourceEdge = Edge.Right;
                destinationEdge = Edge.Left;
            }

            sourceEdgeCenter = sourcePosition.GetEdgeCenter(sourceEdge);
            destinationEdgeCenter = destinationPosition.GetEdgeCenter(destinationEdge);

            siblingStateTransitions.Clear();

            var siblingIndex = 0;

            // Assign one common axis for transition for all siblings,
            // regardless of their inversion. The axis is arbitrarily
            // chosen as the axis for the first transition.
            var assignedTransitionAxis = false;
            var transitionAxis = Vector2.zero;

            foreach (var graphTransition in canvas.graph.transitions)
            {
                var current = transition == graphTransition;

                var analog =
                    transition.source == graphTransition.source &&
                    transition.destination == graphTransition.destination;

                var inverted =
                    transition.source == graphTransition.destination &&
                    transition.destination == graphTransition.source;

                if (current)
                {
                    siblingIndex = siblingStateTransitions.Count;
                }

                if (current || analog || inverted)
                {
                    if (!assignedTransitionAxis)
                    {
                        var siblingStateTransitionDrawer = canvas.Widget<IStateTransitionWidget>(graphTransition);

                        transitionAxis = siblingStateTransitionDrawer.sourceEdge.Normal();

                        assignedTransitionAxis = true;
                    }

                    siblingStateTransitions.Add(graphTransition);
                }
            }

            // Fix the edge case where the source and destination perfectly overlap

            if (transitionAxis == Vector2.zero)
            {
                transitionAxis = Vector2.right;
            }

            // Calculate the spread axis and origin for the set of siblings

            var spreadAxis = transitionAxis.Perpendicular1().Abs();
            var spreadOrigin = (sourceEdgeCenter + destinationEdgeCenter) / 2;

            if (transition.source == transition.destination)
            {
                spreadAxis = Vector2.up;
                spreadOrigin = sourcePosition.GetEdgeCenter(Edge.Bottom) - Vector2.down * 10;
            }

            if (BoltCore.Configuration.developerMode && BoltCore.Configuration.debug)
            {
                Handles.BeginGUI();
                Handles.color = Color.yellow;
                Handles.DrawLine(spreadOrigin + spreadAxis * -1000, spreadOrigin + spreadAxis * 1000);
                Handles.EndGUI();
            }

            // Calculate the offset of the current sibling by iterating over its predecessors

            var spreadOffset = 0f;
            var previousSpreadSize = 0f;

            for (var i = 0; i <= siblingIndex; i++)
            {
                var siblingSize = canvas.Widget<IStateTransitionWidget>(siblingStateTransitions[i]).outerPosition.size;
                var siblingSizeProjection = GraphGUI.SizeProjection(siblingSize, spreadOrigin, spreadAxis);
                spreadOffset += previousSpreadSize / 2 + siblingSizeProjection / 2;
                previousSpreadSize = siblingSizeProjection;
            }

            if (transition.source != transition.destination)
            {
                // Calculate the total spread size to center the sibling set

                var totalSpreadSize = 0f;

                for (var i = 0; i < siblingStateTransitions.Count; i++)
                {
                    var siblingSize = canvas.Widget<IStateTransitionWidget>(siblingStateTransitions[i]).outerPosition.size;
                    var siblingSizeProjection = GraphGUI.SizeProjection(siblingSize, spreadOrigin, spreadAxis);
                    totalSpreadSize += siblingSizeProjection;
                }

                spreadOffset -= totalSpreadSize / 2;
            }

            // Finally, calculate the positions

            middle = spreadOrigin + spreadOffset * spreadAxis;

            var edgeX = middle.x - edgeWidth / 2;
            var edgeY = middle.y - edgeHeight / 2;

            _position = new Rect
                (
                edgeX,
                edgeY,
                edgeWidth,
                edgeHeight
                ).PixelPerfect();

            var innerX = innerPosition.x;
            var innerY = innerPosition.y;

            _clippingPosition = _position.Encompass(sourceEdgeCenter).Encompass(destinationEdgeCenter);

            if (transition.source != transition.destination)
            {
                entryEdge = destinationEdge;
                exitEdge = sourceEdge;
            }
            else
            {
                entryEdge = sourceEdge;
                exitEdge = destinationEdge;
            }

            entryEdgeCenter = edgePosition.GetEdgeCenter(entryEdge);
            exitEdgeCenter = edgePosition.GetEdgeCenter(exitEdge);

            var x = innerX;

            iconPosition = new Rect
                (
                x,
                innerY,
                Styles.eventIcon.fixedWidth,
                Styles.eventIcon.fixedHeight
                ).PixelPerfect();

            x += iconPosition.width;

            var clipWidth = innerWidth - (x - innerX);

            clipPosition = new Rect
                (
                x,
                edgeY,
                clipWidth,
                edgeHeight
                ).PixelPerfect();

            labelInnerPosition = new Rect
                (
                Styles.spaceAroundIcon,
                innerY - edgeY,
                labelWidth,
                labelHeight
                ).PixelPerfect();
        }

        #endregion


        #region Drawing

        protected virtual NodeColorMix baseColor => NodeColor.Gray;

        protected override NodeColorMix color
        {
            get
            {
                if (transitionDebugData.runtimeException != null)
                {
                    return NodeColor.Red;
                }

                var color = baseColor;

                if (analysis.warnings.Count > 0)
                {
                    var mostSevereWarning = Warning.MostSevereLevel(analysis.warnings);

                    switch (mostSevereWarning)
                    {
                        case WarningLevel.Error:
                            color = NodeColor.Red;

                            break;

                        case WarningLevel.Severe:
                            color = NodeColor.Orange;

                            break;

                        case WarningLevel.Caution:
                            color = NodeColor.Yellow;

                            break;
                    }
                }

                if (EditorApplication.isPlaying)
                {
                    if (EditorApplication.isPaused)
                    {
                        if (EditorTimeBinding.frame == transitionDebugData.lastBranchFrame)
                        {
                            color = NodeColor.Blue;
                        }
                    }
                    else
                    {
                        color.blue = Mathf.Lerp(1, 0, (EditorTimeBinding.time - transitionDebugData.lastBranchTime) / StateWidget<IState>.Styles.enterFadeDuration);
                    }
                }

                return color;
            }
        }

        protected override NodeShape shape => NodeShape.Hex;

        private bool revealLabel
        {
            get
            {
                switch (BoltState.Configuration.transitionsReveal)
                {
                    case StateRevealCondition.Always:

                        return true;
                    case StateRevealCondition.Never:

                        return false;
                    case StateRevealCondition.OnHover:

                        return isMouseOver;
                    case StateRevealCondition.OnHoverWithAlt:

                        return isMouseOver && e.alt;
                    case StateRevealCondition.WhenSelected:

                        return selection.Contains(transition);
                    case StateRevealCondition.OnHoverOrSelected:

                        return isMouseOver || selection.Contains(transition);
                    case StateRevealCondition.OnHoverWithAltOrSelected:

                        return isMouseOver && e.alt || selection.Contains(transition);
                    default:

                        throw new UnexpectedEnumValueException<StateRevealCondition>(BoltState.Configuration.transitionsReveal);
                }
            }
        }

        private bool revealedLabel;

        private void CheckReveal()
        {
            var revealLabel = this.revealLabel;

            if (revealLabel != revealedLabel)
            {
                Reposition();
            }

            revealedLabel = revealLabel;
        }

        protected override bool dim
        {
            get
            {
                var dim = BoltCore.Configuration.dimInactiveNodes && !(transition.source.Analysis<StateAnalysis>(context).isEntered && analysis.isTraversed);

                if (isMouseOver || isSelected)
                {
                    dim = false;
                }

                return dim;
            }
        }

        public override void DrawForeground()
        {
            BeginDim();

            base.DrawForeground();

            GUI.Label(iconPosition, label, Styles.eventIcon);

            GUI.BeginClip(clipPosition);

            GUI.Label(labelInnerPosition, label, invertForeground ? Styles.labelInverted : Styles.label);

            GUI.EndClip();

            EndDim();

            CheckReveal();
        }

        public override void DrawBackground()
        {
            BeginDim();

            base.DrawBackground();

            DrawConnection();

            if (showDroplets)
            {
                DrawDroplets();
            }

            EndDim();
        }

        private void DrawConnection()
        {
            GraphGUI.DrawConnectionArrow(Color.white, sourceEdgeCenter, entryEdgeCenter, sourceEdge, entryEdge, relativeBend, minBend);

            if (BoltState.Configuration.transitionsEndArrow)
            {
                GraphGUI.DrawConnectionArrow(Color.white, exitEdgeCenter, destinationEdgeCenter, exitEdge, destinationEdge, relativeBend, minBend);
            }
            else
            {
                GraphGUI.DrawConnection(Color.white, exitEdgeCenter, destinationEdgeCenter, exitEdge, destinationEdge, null, Vector2.zero, relativeBend, minBend);
            }
        }

        #endregion


        #region Selecting

        public override bool canSelect => true;

        #endregion


        #region Dragging

        public override bool canDrag => false;

        protected override bool snapToGrid => false;

        #endregion


        #region Deleting

        public override bool canDelete => true;

        #endregion


        #region Droplets

        private readonly List<float> droplets = new List<float>();

        private float dropTime;

        private float lastBranchTime;

        protected virtual bool showDroplets => BoltState.Configuration.animateTransitions;

        protected virtual Vector2 GetDropletSize()
        {
            return BoltFlow.Icons.valuePortConnected?[12].Size() ?? 12 * Vector2.one;
        }

        protected virtual void DrawDroplet(Rect position)
        {
            GUI.DrawTexture(position, BoltFlow.Icons.valuePortConnected?[12]);
        }

        private void DrawDroplets()
        {
            foreach (var droplet in droplets)
            {
                Vector2 position;

                if (droplet < 0.5f)
                {
                    var t = droplet / 0.5f;
                    position = GraphGUI.GetPointOnConnection(t, sourceEdgeCenter, entryEdgeCenter, sourceEdge, entryEdge, relativeBend, minBend);
                }
                else
                {
                    var t = (droplet - 0.5f) / 0.5f;
                    position = GraphGUI.GetPointOnConnection(t, exitEdgeCenter, destinationEdgeCenter, exitEdge, destinationEdge, relativeBend, minBend);
                }

                var size = GetDropletSize();

                using (LudiqGUI.color.Override(Color.white))
                {
                    DrawDroplet(new Rect(position.x - size.x / 2, position.y - size.y / 2, size.x, size.y));
                }
            }
        }

        #endregion


        public static class Styles
        {
            static Styles()
            {
                label = new GUIStyle(BoltCore.Styles.nodeLabel);
                label.alignment = TextAnchor.MiddleCenter;
                label.imagePosition = ImagePosition.TextOnly;

                labelInverted = new GUIStyle(label);
                labelInverted.normal.textColor = ColorPalette.unityBackgroundDark;

                eventIcon = new GUIStyle();
                eventIcon.imagePosition = ImagePosition.ImageOnly;
                eventIcon.fixedHeight = 16;
                eventIcon.fixedWidth = 16;
            }

            public static readonly GUIStyle label;

            public static readonly GUIStyle labelInverted;

            public static readonly GUIStyle eventIcon;

            public static readonly float spaceAroundIcon = 5;

            public static readonly float revealSpeed = 15;
        }
    }
}