// Copyright 2026 Gregorio Momm
// Licensed under the Apache License, Version 2.0
//
// Ported from RustworkxCore (IBM Qiskit) — Apache License 2.0
// Source: https://github.com/Qiskit/rustworkx/tree/main/rustworkx-core/src

//! Transitivity and clustering coefficient algorithms.
//!
//! - `graph_transitivity` — global clustering coefficient (fraction of triangles)
//! - `graph_node_triangles` — number of triangles through each node
//! - `clustering_coefficient` — local clustering coefficient per node
//! - `average_clustering` — average local clustering coefficient

use std::collections::HashMap;
use std::collections::HashSet;

use super::super::graph::{Graph, NodeId};

// ─── Node triangles ───────────────────────────────────────────────────────────

/// Count the number of triangles each node participates in.
///
/// A triangle is a set of 3 nodes mutually connected.
/// For directed graphs, counts directed triangles.
///
/// Runtime: O(m^{3/2})
pub fn graph_node_triangles(graph: &Graph) -> HashMap<NodeId, usize> {
    let mut triangles: HashMap<NodeId, usize> = graph.nodes().map(|n| (n, 0)).collect();

    // Build neighbor sets for fast intersection
    let neighbor_sets: HashMap<NodeId, HashSet<NodeId>> = graph
        .nodes()
        .map(|n| {
            let nbrs: HashSet<NodeId> = graph.out_neighbors(n).iter().map(|e| e.target).collect();
            (n, nbrs)
        })
        .collect();

    for u in graph.nodes() {
        for v in neighbor_sets[&u].iter().copied() {
            if v <= u { continue; } // Count each edge once
            // Count common neighbors of u and v
            let common = neighbor_sets[&u]
                .intersection(&neighbor_sets[&v])
                .count();
            *triangles.get_mut(&u).unwrap() += common;
            *triangles.get_mut(&v).unwrap() += common;
        }
    }

    // Each triangle is counted twice per node (once per incident edge), so halve
    for v in triangles.values_mut() { *v /= 2; }

    triangles
}

// ─── Global transitivity ──────────────────────────────────────────────────────

/// Compute global graph transitivity (clustering coefficient).
///
/// Transitivity = (3 × number of triangles) / number of connected triples
///
/// Value in [0, 1]. Returns 0 if there are no connected triples.
///
/// Runtime: O(m^{3/2})
pub fn graph_transitivity(graph: &Graph) -> f64 {
    let mut triangles: usize = 0;
    let mut triples: usize = 0;

    let neighbor_sets: HashMap<NodeId, HashSet<NodeId>> = graph
        .nodes()
        .map(|n| {
            let nbrs: HashSet<NodeId> = graph.out_neighbors(n).iter().map(|e| e.target).collect();
            (n, nbrs)
        })
        .collect();

    for u in graph.nodes() {
        let deg = neighbor_sets[&u].len();
        triples += deg * deg.saturating_sub(1) / 2;

        for v in neighbor_sets[&u].iter().copied() {
            if v <= u { continue; }
            triangles += neighbor_sets[&u].intersection(&neighbor_sets[&v]).count();
        }
    }

    if triples == 0 {
        return 0.0;
    }
    // Each triangle is counted 3 times (once per edge), so actual triangles = triangles/3
    // Formula: 3 * actual_triangles / triples = 3 * (triangles/3) / triples = triangles / triples
    triangles as f64 / triples as f64
}

// ─── Local clustering coefficient ────────────────────────────────────────────

/// Compute local clustering coefficient for each node.
///
/// For node u with degree k: C(u) = triangles / (k*(k-1)/2)
/// (for undirected graphs).
///
/// Returns 0 for nodes with degree < 2.
///
/// Runtime: O(n * k²) where k = average degree
pub fn clustering_coefficient(graph: &Graph) -> HashMap<NodeId, f64> {
    let neighbor_sets: HashMap<NodeId, HashSet<NodeId>> = graph
        .nodes()
        .map(|n| {
            let nbrs: HashSet<NodeId> = graph.out_neighbors(n).iter().map(|e| e.target).collect();
            (n, nbrs)
        })
        .collect();

    graph.nodes().map(|u| {
        let nbrs = &neighbor_sets[&u];
        let k = nbrs.len();
        if k < 2 {
            return (u, 0.0);
        }

        let mut connected = 0usize;
        let nbrs_vec: Vec<NodeId> = nbrs.iter().copied().collect();
        for i in 0..nbrs_vec.len() {
            for j in (i+1)..nbrs_vec.len() {
                let a = nbrs_vec[i];
                let b = nbrs_vec[j];
                if neighbor_sets[&a].contains(&b) || neighbor_sets[&b].contains(&a) {
                    connected += 1;
                }
            }
        }

        let possible = k * (k - 1) / 2;
        (u, connected as f64 / possible as f64)
    }).collect()
}

/// Compute the average clustering coefficient over all nodes.
pub fn average_clustering(graph: &Graph) -> f64 {
    let coeffs = clustering_coefficient(graph);
    let n = coeffs.len();
    if n == 0 { return 0.0; }
    coeffs.values().sum::<f64>() / n as f64
}

// ─── Tests ────────────────────────────────────────────────────────────────────

#[cfg(test)]
mod tests {
    use super::*;
    use crate::graph::backends::networkit_rust::graph::GraphConfig;

    fn triangle_graph() -> Graph {
        let mut g = Graph::new(GraphConfig::simple());
        for _ in 0..3 { g.add_node(); }
        g.add_edge(0, 1, None);
        g.add_edge(1, 2, None);
        g.add_edge(2, 0, None);
        g
    }

    fn star_graph() -> Graph {
        let mut g = Graph::new(GraphConfig::simple());
        for _ in 0..5 { g.add_node(); }
        for i in 1..5u64 { g.add_edge(0, i, None); }
        g
    }

    // ── graph_node_triangles ──────────────────────────────────────────────────

    #[test]
    fn test_node_triangles_triangle() {
        let g = triangle_graph();
        let t = graph_node_triangles(&g);
        // Each node is in exactly 1 triangle
        for node in g.nodes() {
            assert_eq!(t[&node], 1, "node {} should be in 1 triangle", node);
        }
    }

    #[test]
    fn test_node_triangles_star() {
        // Star: no triangles
        let g = star_graph();
        let t = graph_node_triangles(&g);
        for (&node, &count) in &t {
            assert_eq!(count, 0, "node {} should be in 0 triangles", node);
        }
    }

    #[test]
    fn test_node_triangles_k4() {
        // K4: every node is in C(3,2)=3 triangles
        let mut g = Graph::new(GraphConfig::simple());
        for _ in 0..4 { g.add_node(); }
        for i in 0..4u64 { for j in (i+1)..4 { g.add_edge(i, j, None); } }
        let t = graph_node_triangles(&g);
        for node in g.nodes() {
            assert_eq!(t[&node], 3, "K4 node {} should be in 3 triangles", node);
        }
    }

    // ── graph_transitivity ────────────────────────────────────────────────────

    #[test]
    fn test_transitivity_triangle() {
        let g = triangle_graph();
        let trans = graph_transitivity(&g);
        // Perfect triangle → transitivity = 1
        assert!((trans - 1.0).abs() < 1e-9, "triangle transitivity should be 1, got {}", trans);
    }

    #[test]
    fn test_transitivity_star() {
        let g = star_graph();
        let trans = graph_transitivity(&g);
        // No triangles in star → transitivity = 0
        assert!((trans - 0.0).abs() < 1e-9, "star transitivity should be 0, got {}", trans);
    }

    #[test]
    fn test_transitivity_path() {
        let mut g = Graph::new(GraphConfig::simple());
        for _ in 0..4 { g.add_node(); }
        for i in 0..3u64 { g.add_edge(i, i+1, None); }
        let trans = graph_transitivity(&g);
        // Path has no triangles → 0
        assert!((trans - 0.0).abs() < 1e-9);
    }

    // ── clustering_coefficient ────────────────────────────────────────────────

    #[test]
    fn test_clustering_triangle() {
        let g = triangle_graph();
        let cc = clustering_coefficient(&g);
        for node in g.nodes() {
            assert!(
                (cc[&node] - 1.0).abs() < 1e-9,
                "node {} in triangle should have cc=1, got {}",
                node, cc[&node]
            );
        }
    }

    #[test]
    fn test_clustering_star() {
        let g = star_graph();
        let cc = clustering_coefficient(&g);
        // Leaves have degree 1 → 0
        assert_eq!(cc[&1], 0.0);
        // Center has degree 4, no edges among leaves → 0
        assert_eq!(cc[&0], 0.0);
    }

    #[test]
    fn test_average_clustering_triangle() {
        let g = triangle_graph();
        let avg = average_clustering(&g);
        assert!((avg - 1.0).abs() < 1e-9);
    }

    #[test]
    fn test_average_clustering_empty() {
        let g = Graph::new(GraphConfig::simple());
        let avg = average_clustering(&g);
        assert_eq!(avg, 0.0);
    }
}
