using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using AIStudio.Wpf.DiagramDesigner.Geometrys;

namespace AIStudio.Wpf.DiagramDesigner.Layout
{
    internal class Engine
    {
        internal void ApplyRandomShift(DiagramViewModel network)
        {
            Random random = new Random();
            foreach (var node in network.Items.OfType<DesignerItemViewModelBase>())
            {
                node.Position = node.Position + new VectorBase(random.NextDouble(), random.NextDouble());
            }
        }

        internal void Update(int deltaTMillis, IState state, Configuration config)
        {
            // Calculate forces
            int nodeCount = config.Network.Items.Count;
            IList<(DesignerItemViewModelBase, VectorBase)> nodeForces = new List<(DesignerItemViewModelBase, VectorBase)>(nodeCount);

            foreach (var node in config.Network.Items.OfType<DesignerItemViewModelBase>())
            {
                if (!config.IsFixedNode(node))
                {
                    nodeForces.Add((node, CalculateNodeForce(node, state, config)));
                }
            }

            // Apply forces
            foreach (var (node, force) in nodeForces)
            {
                VectorBase speed = state.GetNodeSpeed(node);
                VectorBase pos = state.GetNodePosition(node);
                double deltaT = deltaTMillis / 1000.0;
                state.SetNodePosition(node, pos + ((speed * deltaT) + (force * deltaT * deltaT / 2)));
                state.SetNodeSpeed(node, speed + ((force / config.NodeMass(node)) * deltaT));
            }
        }

        private VectorBase CalculateNodeForce(DesignerItemViewModelBase node, IState state, Configuration config)
        {
            VectorBase force = new VectorBase();

            // Calculate total force on node from endpoints
            if (node.InputConnectors.Count > 0 || node.OutputConnectors.Count > 0)
            {
                force += node.InputConnectors.Concat(node.OutputConnectors)
                    .Select(e => CalculateEndpointForce(e, state, config))
                    .Aggregate((v1, v2) => v1 + v2);
            }

            // Apply node repulsion force so nodes don't overlap
            var nodeCenter = state.GetNodePosition(node) + (new VectorBase(node.Size.Width, node.Size.Height) / 2.0);
            foreach (var otherNode in config.Network.Items.OfType<DesignerItemViewModelBase>())
            {
                if (node == otherNode)
                {
                    continue;
                }

                var otherNodeCenter = state.GetNodePosition(otherNode) + (new VectorBase(otherNode.Size.Width, otherNode.Size.Height) / 2.0);
                var thisToOther = otherNodeCenter - nodeCenter;
                var dist = thisToOther.Length;
                thisToOther.Normalize();

                var repulsionX = thisToOther.X * (-1 * ((node.Size.Width + otherNode.Size.Width) / 2) / dist);
                var repulsionY = thisToOther.Y * (-1 * ((node.Size.Height + otherNode.Size.Height) / 2) / dist);
                force += new VectorBase(repulsionX, repulsionY) * config.NodeRepulsionForce;
            }

            // Apply friction to make the movement converge to a stable state.
            float gravity = 9.8f;
            float normalForce = gravity * config.NodeMass(node);
            float kineticFriction = normalForce * config.FrictionCoefficient(node);
            VectorBase frictionVector = new VectorBase();
            var nodeSpeed = state.GetNodeSpeed(node);
            if (nodeSpeed.Length > 0)
            {
                frictionVector = new VectorBase(nodeSpeed.X, nodeSpeed.Y);
                frictionVector.Normalize();
                frictionVector *= -1.0 * kineticFriction;
            }
            force += frictionVector;

            return force;
        }

        private VectorBase CalculateEndpointForce(ConnectorInfoBase endpoint, IState state, Configuration config)
        {
            var pos = state.GetEndpointPosition(endpoint);

            VectorBase force = new VectorBase();

            foreach (var conn in config.Network.Items.OfType<ConnectionViewModel>().Where(p => p.SinkConnectorInfo == endpoint || p.SourceConnectorInfo == endpoint).ToList())
            {
                var otherSide = conn.SourceConnectorInfo == endpoint ? conn.SinkConnectorInfo : conn.SourceConnectorInfo;
                var otherSidePos = state.GetEndpointPosition(otherSide);
                var dist = (otherSidePos - pos).Length;
                var angle = Math.Acos((otherSidePos.X - pos.X) / dist);
                if (otherSidePos.Y < pos.Y)
                {
                    angle *= -1.0;
                }

                // Put a spring between connected endpoints.
                var hookForce = (dist - config.EquilibriumDistance(conn)) * config.SpringConstant(conn);
                force += new VectorBase(Math.Cos(angle), Math.Sin(angle)) * hookForce;

                // Try to 'straighten' out the graph horizontally.
                var isLeftSide = endpoint.Orientation == ConnectorOrientation.Left;
                var rowForce = (isLeftSide ? 1 : -1) * config.RowForce(conn);
                force.X += rowForce;
            }

            return force;
        }
    }
}
