// Copyright 2026 Gregorio Momm
// Licensed under the Apache License, Version 2.0
//
// This file implements a high-performance graph data structure inspired by NetworKit's
// design patterns, ported from C++ to pure Rust.
//
// NetworKit: https://networkit.github.io/ (MIT License)
// Algorithm designs and architectural patterns are based on NetworKit C++, but
// implementation is original Rust code adapted for Cypher property graph semantics.

//! NetworKit-inspired graph implementation in pure Rust.
//!
//! This module ports NetworKit's high-performance adjacency list design to idiomatic Rust,
//! providing zero-cost abstractions and cache-friendly memory layout.
//!
//! Design principles from NetworKit:
//! - Contiguous adjacency vectors for cache locality
//! - Optional features (weighted, directed, indexed) to avoid overhead
//! - Separate in/out neighbor lists for directed graphs
//! - Memory preallocation and compaction for performance
//!
//! Reference: <https://networkit.github.io/dev-docs/cpp_api/classNetworKit_1_1Graph.html>

use rustc_hash::FxHashSet;

/// Node identifier (same as NetworKit's node type).
pub type NodeId = u64;

/// Edge identifier for indexed edges.
pub type EdgeId = u64;

/// Edge weight (default 1.0 for unweighted graphs).
pub type Weight = f64;

/// A neighbor in the adjacency list, with optional weight and edge ID.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Neighbor {
    pub target: NodeId,
    pub weight: Option<Weight>,
    pub edge_id: Option<EdgeId>,
    /// Whether this is a reverse reference (for incoming edges in directed graphs).
    /// When true, this neighbor represents an incoming edge stored in the in_neighbors list.
    /// The primary edge is always in out_neighbors with is_reverse=false.
    pub is_reverse: bool,
}

impl Neighbor {
    /// Create a simple neighbor (unweighted, no edge ID).
    #[inline]
    pub fn simple(target: NodeId) -> Self {
        Self {
            target,
            weight: None,
            edge_id: None,
            is_reverse: false,
        }
    }

    /// Create a weighted neighbor.
    #[inline]
    pub fn weighted(target: NodeId, weight: Weight) -> Self {
        Self {
            target,
            weight: Some(weight),
            edge_id: None,
            is_reverse: false,
        }
    }

    /// Create a fully indexed neighbor.
    #[inline]
    pub fn indexed(target: NodeId, weight: Option<Weight>, edge_id: EdgeId) -> Self {
        Self {
            target,
            weight,
            edge_id: Some(edge_id),
            is_reverse: false,
        }
    }

    /// Create a reverse neighbor reference (for incoming edges).
    #[inline]
    pub fn reverse(target: NodeId, weight: Option<Weight>, edge_id: EdgeId) -> Self {
        Self {
            target,
            weight,
            edge_id: Some(edge_id),
            is_reverse: true,
        }
    }

    /// Check if this is a reverse reference.
    #[inline]
    pub fn is_reverse(&self) -> bool {
        self.is_reverse
    }
}

/// Configuration options for graph construction.
#[derive(Debug, Clone, Copy)]
#[derive(Default)]
pub struct GraphConfig {
    /// Enable edge weights (default: false).
    pub weighted: bool,
    /// Use directed edges (default: false).
    pub directed: bool,
    /// Maintain edge IDs for O(1) edge lookup (default: false).
    pub indexed: bool,
    /// Estimated number of nodes for preallocation.
    pub expected_nodes: Option<usize>,
    /// Estimated average degree for preallocation.
    pub expected_avg_degree: Option<usize>,
}


impl GraphConfig {
    /// Create configuration for a simple unweighted, undirected graph.
    pub fn simple() -> Self {
        Self::default()
    }

    /// Create configuration for a weighted graph.
    pub fn weighted() -> Self {
        Self {
            weighted: true,
            ..Default::default()
        }
    }

    /// Create configuration for a directed graph.
    pub fn directed() -> Self {
        Self {
            directed: true,
            ..Default::default()
        }
    }

    /// Enable edge indexing for O(1) edge lookup.
    pub fn with_edge_index(mut self) -> Self {
        self.indexed = true;
        self
    }

    /// Set expected graph size for preallocation.
    pub fn with_capacity(mut self, nodes: usize, avg_degree: usize) -> Self {
        self.expected_nodes = Some(nodes);
        self.expected_avg_degree = Some(avg_degree);
        self
    }
}

/// High-performance graph using adjacency list representation.
///
/// Inspired by NetworKit's Graph class, optimized for:
/// - Cache-friendly memory layout
/// - Minimal overhead for unweighted/undirected cases
/// - Fast iteration and neighbor access
/// - Support for node/edge deletion with ID reuse
#[derive(Debug)]
pub struct Graph {
    /// Configuration flags.
    config: GraphConfig,

    /// Outgoing edges (for directed) or all edges (for undirected).
    /// Index by NodeId, contains list of neighbors.
    out_neighbors: Vec<Vec<Neighbor>>,

    /// Incoming edges (only for directed graphs).
    in_neighbors: Option<Vec<Vec<Neighbor>>>,

    /// Set of deleted node IDs available for reuse.
    deleted_nodes: FxHashSet<NodeId>,

    /// Upper bound on node IDs (max assigned ID + 1).
    upper_node_id_bound: NodeId,

    /// Current number of active nodes.
    node_count: usize,

    /// Current number of edges (directed: counts each direction separately for undirected).
    edge_count: usize,

    /// Next edge ID to assign (only used if indexed == true).
    next_edge_id: EdgeId,
}

impl Graph {
    /// Create a new graph with the given configuration.
    pub fn new(config: GraphConfig) -> Self {
        let mut graph = Self {
            config,
            out_neighbors: Vec::new(),
            in_neighbors: if config.directed {
                Some(Vec::new())
            } else {
                None
            },
            deleted_nodes: FxHashSet::default(),
            upper_node_id_bound: 0,
            node_count: 0,
            edge_count: 0,
            next_edge_id: 0,
        };

        // Preallocate if capacity hints provided
        if let (Some(nodes), Some(avg_degree)) = (config.expected_nodes, config.expected_avg_degree)
        {
            graph.preallocate(nodes, avg_degree);
        }

        graph
    }

    /// Create a simple unweighted, undirected graph.
    pub fn new_simple() -> Self {
        Self::new(GraphConfig::simple())
    }

    /// Create a weighted, undirected graph.
    pub fn new_weighted() -> Self {
        Self::new(GraphConfig::weighted())
    }

    /// Create a directed graph.
    pub fn new_directed() -> Self {
        Self::new(GraphConfig::directed())
    }

    /// Preallocate memory for expected graph size.
    ///
    /// This follows NetworKit's pattern of reserving capacity to avoid
    /// reallocations during graph construction.
    pub fn preallocate(&mut self, nodes: usize, avg_degree: usize) {
        self.out_neighbors.reserve(nodes);

        // Preallocate adjacency vectors based on expected degree
        for _ in 0..nodes {
            let out_vec = Vec::with_capacity(avg_degree);
            self.out_neighbors.push(out_vec);

            if let Some(ref mut in_neighbors) = self.in_neighbors {
                in_neighbors.push(Vec::with_capacity(avg_degree));
            }
        }
    }

    /// Shrink allocated memory to fit actual graph size (NetworKit's shrinkToFit).
    pub fn shrink_to_fit(&mut self) {
        self.out_neighbors.shrink_to_fit();
        for neighbors in &mut self.out_neighbors {
            neighbors.shrink_to_fit();
        }

        if let Some(ref mut in_neighbors) = self.in_neighbors {
            in_neighbors.shrink_to_fit();
            for neighbors in in_neighbors.iter_mut() {
                neighbors.shrink_to_fit();
            }
        }

        self.deleted_nodes.shrink_to_fit();
    }

    // === Node Operations ===

    /// Add a new node and return its ID.
    ///
    /// Reuses deleted node IDs when available (NetworKit's restoreNode pattern).
    pub fn add_node(&mut self) -> NodeId {
        if let Some(&node_id) = self.deleted_nodes.iter().next() {
            // Reuse a deleted node ID
            self.deleted_nodes.remove(&node_id);
            self.node_count += 1;
            node_id
        } else {
            // Allocate a new node ID
            let node_id = self.upper_node_id_bound;
            self.upper_node_id_bound += 1;
            self.node_count += 1;

            // Ensure adjacency lists exist
            self.out_neighbors.push(Vec::new());
            if let Some(ref mut in_neighbors) = self.in_neighbors {
                in_neighbors.push(Vec::new());
            }

            node_id
        }
    }

    /// Add multiple nodes at once and return the range of IDs.
    ///
    /// More efficient than calling add_node() repeatedly.
    pub fn add_nodes(&mut self, count: usize) -> std::ops::Range<NodeId> {
        let start = self.upper_node_id_bound;

        // For bulk addition, don't reuse deleted IDs (simpler and faster)
        for _ in 0..count {
            self.out_neighbors.push(Vec::new());
            if let Some(ref mut in_neighbors) = self.in_neighbors {
                in_neighbors.push(Vec::new());
            }
        }

        self.upper_node_id_bound += count as NodeId;
        self.node_count += count;

        start..self.upper_node_id_bound
    }

    /// Check if a node exists (not deleted).
    #[inline]
    pub fn has_node(&self, node_id: NodeId) -> bool {
        node_id < self.upper_node_id_bound && !self.deleted_nodes.contains(&node_id)
    }

    /// Remove a node and all its incident edges.
    ///
    /// The node ID is marked as deleted and can be reused by future add_node() calls.
    pub fn remove_node(&mut self, node_id: NodeId) -> bool {
        if !self.has_node(node_id) {
            return false;
        }

        // Remove all incident edges
        let node_idx = node_id as usize;

        // Remove outgoing edges
        let out_neighbors = std::mem::take(&mut self.out_neighbors[node_idx]);

        // For undirected graphs, also remove reverse edges
        if !self.config.directed {
            // For undirected, we store each edge twice, so decrement by 2x
            self.edge_count -= out_neighbors.len() * 2;
            for neighbor in &out_neighbors {
                Self::remove_edge_from_list_static(&mut self.out_neighbors[neighbor.target as usize], node_id);
            }
        } else {
            // For directed graphs, remove incoming edges
            if let Some(ref mut in_neighbors) = self.in_neighbors {
                let in_neighbors_list = std::mem::take(&mut in_neighbors[node_idx]);
                self.edge_count -= in_neighbors_list.len();

                // Remove reverse edges from outgoing lists
                for neighbor in &in_neighbors_list {
                    Self::remove_edge_from_list_static(
                        &mut self.out_neighbors[neighbor.target as usize],
                        node_id,
                    );
                }
            }
        }

        // Mark node as deleted
        self.deleted_nodes.insert(node_id);
        self.node_count -= 1;

        true
    }

    /// Helper: Remove an edge from a neighbor list (static method to avoid borrow issues).
    fn remove_edge_from_list_static(neighbors: &mut Vec<Neighbor>, target: NodeId) {
        if let Some(pos) = neighbors.iter().position(|n| n.target == target) {
            neighbors.swap_remove(pos); // O(1) removal using swap
        }
    }

    /// Get the number of active nodes.
    #[inline]
    pub fn node_count(&self) -> usize {
        self.node_count
    }

    /// Return `true` if this is a directed graph.
    #[inline]
    pub fn is_directed(&self) -> bool {
        self.config.directed
    }

    /// Get the upper bound on node IDs.
    #[inline]
    pub fn upper_node_id_bound(&self) -> NodeId {
        self.upper_node_id_bound
    }

    // === Edge Operations ===

    /// Add an edge between two nodes.
    ///
    /// For undirected graphs, adds the edge in both directions.
    /// Returns true if the edge was added, false if it already existed.
    pub fn add_edge(&mut self, source: NodeId, target: NodeId, weight: Option<Weight>) -> bool {
        if !self.has_node(source) || !self.has_node(target) {
            return false;
        }

        // CRITICAL FIX: Removed duplicate edge check to support multigraphs.
        // Cypher allows multiple relationships between the same nodes (even with same type).
        // The original check prevented this, causing MERGE and CREATE to fail when multiple
        // relationships exist between nodes.
        //
        // Note: If duplicate prevention is needed, it should be done at a higher level
        // (in create_relationship) based on relationship type and properties, not here.

        let source_idx = source as usize;
        let edge_id = if self.config.indexed {
            let id = self.next_edge_id;
            self.next_edge_id += 1;
            Some(id)
        } else {
            None
        };

        // Add outgoing edge
        let neighbor = match (weight, edge_id) {
            (Some(w), Some(id)) => Neighbor::indexed(target, Some(w), id),
            (Some(w), None) => Neighbor::weighted(target, w),
            (None, Some(id)) => Neighbor::indexed(target, None, id),
            (None, None) => Neighbor::simple(target),
        };

        self.out_neighbors[source_idx].push(neighbor);

        if self.config.directed {
            self.edge_count += 1;
            // For directed graphs, add to incoming edge list as a REVERSE reference
            // This allows fast incoming edge lookup while maintaining single-direction semantics
            if let Some(ref mut in_neighbors) = self.in_neighbors {
                // Create reverse neighbor with is_reverse=true
                let reverse_neighbor = if let Some(id) = edge_id {
                    Neighbor::reverse(source, weight, id)
                } else {
                    // If no edge_id, we can't properly mark as reverse (shouldn't happen with indexed=true)
                    match (weight, edge_id) {
                        (Some(w), None) => Neighbor::weighted(source, w),
                        (None, None) => Neighbor::simple(source),
                        _ => unreachable!(),
                    }
                };
                in_neighbors[target as usize].push(reverse_neighbor);
            }
        } else {
            // For undirected graphs, add reverse edge and count both directions
            self.edge_count += 2; // Count both directions
            let reverse_neighbor = match (weight, edge_id) {
                (Some(w), Some(id)) => Neighbor::indexed(source, Some(w), id),
                (Some(w), None) => Neighbor::weighted(source, w),
                (None, Some(id)) => Neighbor::indexed(source, None, id),
                (None, None) => Neighbor::simple(source),
            };
            self.out_neighbors[target as usize].push(reverse_neighbor);
        }

        true
    }

    /// Remove an edge between two nodes.
    pub fn remove_edge(&mut self, source: NodeId, target: NodeId) -> bool {
        if !self.has_node(source) || !self.has_node(target) {
            return false;
        }

        let source_idx = source as usize;
        let removed = if let Some(pos) = self.out_neighbors[source_idx]
            .iter()
            .position(|n| n.target == target)
        {
            self.out_neighbors[source_idx].swap_remove(pos);
            true
        } else {
            false
        };

        if removed {
            if self.config.directed {
                self.edge_count -= 1;
                // Remove from incoming edge list
                if let Some(ref mut in_neighbors) = self.in_neighbors {
                    Self::remove_edge_from_list_static(&mut in_neighbors[target as usize], source);
                }
            } else {
                self.edge_count -= 2; // Decrement both directions for undirected
                // Remove reverse edge for undirected graph
                Self::remove_edge_from_list_static(&mut self.out_neighbors[target as usize], source);
            }
        }

        removed
    }

    /// Check if an edge exists.
    #[inline]
    pub fn has_edge(&self, source: NodeId, target: NodeId) -> bool {
        if !self.has_node(source) {
            return false;
        }

        self.out_neighbors[source as usize]
            .iter()
            .any(|n| n.target == target)
    }

    /// Get the number of edges.
    ///
    /// For undirected graphs, this counts each edge once (not twice).
    #[inline]
    pub fn edge_count(&self) -> usize {
        if self.config.directed {
            self.edge_count
        } else {
            self.edge_count / 2 // Undirected edges are stored twice
        }
    }

    // === Neighbor Access ===

    /// Get outgoing neighbors of a node.
    #[inline]
    pub fn out_neighbors(&self, node_id: NodeId) -> &[Neighbor] {
        if node_id >= self.upper_node_id_bound {
            &[]
        } else {
            &self.out_neighbors[node_id as usize]
        }
    }

    /// Get incoming neighbors of a node (directed graphs only).
    #[inline]
    pub fn in_neighbors(&self, node_id: NodeId) -> &[Neighbor] {
        if node_id >= self.upper_node_id_bound {
            return &[];
        }

        if let Some(ref in_neighbors) = self.in_neighbors {
            &in_neighbors[node_id as usize]
        } else {
            &[] // Undirected graph has no separate incoming neighbors
        }
    }

    /// Get the degree (number of neighbors) of a node.
    #[inline]
    pub fn degree(&self, node_id: NodeId) -> usize {
        self.out_neighbors(node_id).len()
    }

    /// Get the in-degree of a node (directed graphs only).
    #[inline]
    pub fn in_degree(&self, node_id: NodeId) -> usize {
        self.in_neighbors(node_id).len()
    }

    // === Iteration ===

    /// Iterate over all active node IDs.
    pub fn nodes(&self) -> impl Iterator<Item = NodeId> + '_ {
        (0..self.upper_node_id_bound).filter(move |&id| !self.deleted_nodes.contains(&id))
    }

    /// Iterate over all edges as (source, target, weight, edge_id).
    /// For directed graphs, only returns primary edges (filters out reverse references).
    pub fn edges(&self) -> impl Iterator<Item = (NodeId, NodeId, Option<Weight>, Option<EdgeId>)> + '_ {
        self.nodes().flat_map(move |source| {
            self.out_neighbors(source)
                .iter()
                .filter(|neighbor| !neighbor.is_reverse()) // Filter out reverse references
                .map(move |neighbor| {
                    (source, neighbor.target, neighbor.weight, neighbor.edge_id)
                })
        })
    }

    /// Execute a function for each edge (NetworKit's forEdges pattern).
    pub fn for_edges<F>(&self, mut f: F)
    where
        F: FnMut(NodeId, NodeId, Option<Weight>),
    {
        for (source, target, weight, _) in self.edges() {
            f(source, target, weight);
        }
    }

    /// Execute a function for each neighbor of a node.
    pub fn for_neighbors_of<F>(&self, node_id: NodeId, mut f: F)
    where
        F: FnMut(NodeId, Option<Weight>),
    {
        for neighbor in self.out_neighbors(node_id) {
            f(neighbor.target, neighbor.weight);
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_simple_graph() {
        let mut graph = Graph::new_simple();

        let n1 = graph.add_node();
        let n2 = graph.add_node();
        let n3 = graph.add_node();

        assert_eq!(graph.node_count(), 3);

        graph.add_edge(n1, n2, None);
        graph.add_edge(n2, n3, None);

        assert_eq!(graph.edge_count(), 2);
        assert!(graph.has_edge(n1, n2));
        assert!(graph.has_edge(n2, n1)); // Undirected
    }

    #[test]
    fn test_directed_graph() {
        let mut graph = Graph::new_directed();

        let n1 = graph.add_node();
        let n2 = graph.add_node();

        graph.add_edge(n1, n2, None);

        assert!(graph.has_edge(n1, n2));
        assert!(!graph.has_edge(n2, n1)); // Directed
    }

    #[test]
    fn test_weighted_graph() {
        let mut graph = Graph::new_weighted();

        let n1 = graph.add_node();
        let n2 = graph.add_node();

        graph.add_edge(n1, n2, Some(2.5));

        let neighbors = graph.out_neighbors(n1);
        assert_eq!(neighbors.len(), 1);
        assert_eq!(neighbors[0].weight, Some(2.5));
    }

    #[test]
    fn test_node_deletion() {
        let mut graph = Graph::new_simple();

        let n1 = graph.add_node();
        let n2 = graph.add_node();
        let n3 = graph.add_node();

        graph.add_edge(n1, n2, None);
        graph.add_edge(n2, n3, None);

        graph.remove_node(n2);

        assert_eq!(graph.node_count(), 2);
        assert_eq!(graph.edge_count(), 0); // Both edges removed
        assert!(!graph.has_node(n2));
    }

    #[test]
    fn test_node_reuse() {
        let mut graph = Graph::new_simple();

        let n1 = graph.add_node();
        graph.remove_node(n1);

        let n2 = graph.add_node();
        assert_eq!(n1, n2); // Should reuse the ID
    }
}
