using System;
using System.Collections.Generic;
using DataStructures.DisjointSet;

namespace Algorithms.Graph.MinimumSpanningTree
{
    /// <summary>
    ///     Algorithm to determine the minimum spanning forest of an undirected graph.
    /// </summary>
    /// <remarks>
    ///     Kruskal's algorithm is a greedy algorithm that can determine the
    ///     minimum spanning tree or minimum spanning forest of any undirected
    ///     graph. Unlike Prim's algorithm, Kruskal's algorithm will work on
    ///     graphs that are unconnected. This algorithm will always have a
    ///     running time of O(E log V) where E is the number of edges and V is
    ///     the number of vertices/nodes.
    ///     More information: https://en.wikipedia.org/wiki/Kruskal%27s_algorithm .
    ///     Pseudocode and analysis: https://www.personal.kent.edu/~rmuhamma/Algorithms/MyAlgorithms/GraphAlgor/primAlgor.htm .
    /// </remarks>
    public static class Kruskal
    {
        /// <summary>
        ///     Determine the minimum spanning tree/forest of the given graph.
        /// </summary>
        /// <param name="adjacencyMatrix">Adjacency matrix representing the graph.</param>
        /// <returns>Adjacency matrix of the minimum spanning tree/forest.</returns>
        public static float[,] Solve(float[,] adjacencyMatrix)
        {
            ValidateGraph(adjacencyMatrix);

            var numNodes = adjacencyMatrix.GetLength(0);
            var set = new DisjointSet<int>();
            var nodes = new Node<int>[numNodes];
            var edgeWeightList = new List<float>();
            var nodeConnectList = new List<(int, int)>();

            // Add nodes to disjoint set
            for (var i = 0; i < numNodes; i++)
            {
                nodes[i] = set.MakeSet(i);
            }

            // Create lists with edge weights and associated connectivity
            for (var i = 0; i < numNodes - 1; i++)
            {
                for (var j = i + 1; j < numNodes; j++)
                {
                    if (float.IsFinite(adjacencyMatrix[i, j]))
                    {
                        edgeWeightList.Add(adjacencyMatrix[i, j]);
                        nodeConnectList.Add((i, j));
                    }
                }
            }

            var edges = Solve(set, nodes, edgeWeightList.ToArray(), nodeConnectList.ToArray());

            // Initialize minimum spanning tree
            var mst = new float[numNodes, numNodes];
            for (var i = 0; i < numNodes; i++)
            {
                mst[i, i] = float.PositiveInfinity;

                for (var j = i + 1; j < numNodes; j++)
                {
                    mst[i, j] = float.PositiveInfinity;
                    mst[j, i] = float.PositiveInfinity;
                }
            }

            foreach (var (node1, node2) in edges)
            {
                mst[node1, node2] = adjacencyMatrix[node1, node2];
                mst[node2, node1] = adjacencyMatrix[node1, node2];
            }

            return mst;
        }

        /// <summary>
        ///     Determine the minimum spanning tree/forest of the given graph.
        /// </summary>
        /// <param name="adjacencyList">Adjacency list representing the graph.</param>
        /// <returns>Adjacency list of the minimum spanning tree/forest.</returns>
        public static Dictionary<int, float>[] Solve(Dictionary<int, float>[] adjacencyList)
        {
            ValidateGraph(adjacencyList);

            var numNodes = adjacencyList.Length;
            var set = new DisjointSet<int>();
            var nodes = new Node<int>[numNodes];
            var edgeWeightList = new List<float>();
            var nodeConnectList = new List<(int, int)>();

            // Add nodes to disjoint set and create list of edge weights and associated connectivity
            for (var i = 0; i < numNodes; i++)
            {
                nodes[i] = set.MakeSet(i);

                foreach(var (node, weight) in adjacencyList[i])
                {
                    edgeWeightList.Add(weight);
                    nodeConnectList.Add((i, node));
                }
            }

            var edges = Solve(set, nodes, edgeWeightList.ToArray(), nodeConnectList.ToArray());

            // Create minimum spanning tree
            var mst = new Dictionary<int, float>[numNodes];
            for (var i = 0; i < numNodes; i++)
            {
                mst[i] = new Dictionary<int, float>();
            }

            foreach (var (node1, node2) in edges)
            {
                mst[node1].Add(node2, adjacencyList[node1][node2]);
                mst[node2].Add(node1, adjacencyList[node1][node2]);
            }

            return mst;
        }

        /// <summary>
        ///     Ensure that the given graph is undirected.
        /// </summary>
        /// <param name="adj">Adjacency matrix of graph to check.</param>
        private static void ValidateGraph(float[,] adj)
        {
            if (adj.GetLength(0) != adj.GetLength(1))
            {
                throw new ArgumentException("Matrix must be square!");
            }

            for (var i = 0; i < adj.GetLength(0) - 1; i++)
            {
                for (var j = i + 1; j < adj.GetLength(1); j++)
                {
                    if (Math.Abs(adj[i, j] - adj[j, i]) > 1e-6)
                    {
                        throw new ArgumentException("Matrix must be symmetric!");
                    }
                }
            }
        }

        /// <summary>
        ///     Ensure that the given graph is undirected.
        /// </summary>
        /// <param name="adj">Adjacency list of graph to check.</param>
        private static void ValidateGraph(Dictionary<int, float>[] adj)
        {
            for (var i = 0; i < adj.Length; i++)
            {
                foreach (var edge in adj[i])
                {
                    if (!adj[edge.Key].ContainsKey(i) || Math.Abs(edge.Value - adj[edge.Key][i]) > 1e-6)
                    {
                        throw new ArgumentException("Graph must be undirected!");
                    }
                }
            }
        }

        /// <summary>
        ///     Determine the minimum spanning tree/forest.
        /// </summary>
        /// <param name="set">Disjoint set needed for set operations.</param>
        /// <param name="nodes">List of nodes in disjoint set associated with each node.</param>
        /// <param name="edgeWeights">Weights of each edge.</param>
        /// <param name="connections">Nodes associated with each item in the <paramref name="edgeWeights"/> parameter.</param>
        /// <returns>Array of edges in the minimum spanning tree/forest.</returns>
        private static (int, int)[] Solve(DisjointSet<int> set, Node<int>[] nodes, float[] edgeWeights, (int, int)[] connections)
        {
            var edges = new List<(int, int)>();

            Array.Sort(edgeWeights, connections);

            foreach (var (node1, node2) in connections)
            {
                if (set.FindSet(nodes[node1]) != set.FindSet(nodes[node2]))
                {
                    set.UnionSet(nodes[node1], nodes[node2]);
                    edges.Add((node1, node2));
                }
            }

            return edges.ToArray();
        }
    }
}
