// 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/isomorphism
//
// Academic References:
// - Cordella, L.P., Foggia, P., Sansone, C., & Vento, M. (2004).
//   "A (sub)graph isomorphism algorithm for matching large graphs."
//   IEEE Transactions on Pattern Analysis and Machine Intelligence, 26(10), 1367-1372.
//   (VF2 algorithm)

//! Graph isomorphism algorithms.
//!
//! - `is_isomorphic`           — check if two graphs are structurally identical
//! - `vf2_mapping`             — find one isomorphism mapping between two graphs
//! - `is_subgraph_isomorphic`  — check if `pattern` is isomorphic to a subgraph of `graph`
//! - `subgraph_vf2_mapping`    — find one subgraph isomorphism mapping
//! - `vf2_all_mappings`        — enumerate all isomorphism mappings (up to a limit)

use super::super::graph::{Graph, NodeId};
use std::collections::HashMap;

// ─── VF2 State ────────────────────────────────────────────────────────────────

/// Internal VF2 matching state.
struct Vf2State<'a> {
    g1: &'a Graph,
    g2: &'a Graph,
    /// core_1[i] = j means node i in g1 is mapped to node j in g2. MAX = unmapped.
    core_1: Vec<NodeId>,
    /// core_2[j] = i means node j in g2 is mapped to node i in g1. MAX = unmapped.
    core_2: Vec<NodeId>,
    /// in_1[i] = depth at which node i entered T1_in (predecessor frontier), 0 = not in
    in_1: Vec<usize>,
    /// out_1[i] = depth at which node i entered T1_out (successor frontier), 0 = not in
    out_1: Vec<usize>,
    in_2: Vec<usize>,
    out_2: Vec<usize>,
    depth: usize,
    nodes_1: Vec<NodeId>,
    nodes_2: Vec<NodeId>,
    n1: usize,
    n2: usize,
}

const UNMAP: NodeId = NodeId::MAX;

impl<'a> Vf2State<'a> {
    fn new(g1: &'a Graph, g2: &'a Graph) -> Self {
        let nodes_1: Vec<NodeId> = g1.nodes().collect();
        let nodes_2: Vec<NodeId> = g2.nodes().collect();
        let n1 = g1.upper_node_id_bound() as usize;
        let n2 = g2.upper_node_id_bound() as usize;
        Self {
            g1, g2,
            core_1: vec![UNMAP; n1],
            core_2: vec![UNMAP; n2],
            in_1:  vec![0; n1], out_1: vec![0; n1],
            in_2:  vec![0; n2], out_2: vec![0; n2],
            depth: 0,
            n1, n2,
            nodes_1, nodes_2,
        }
    }

    fn add_pair(&mut self, u: NodeId, v: NodeId) {
        self.depth += 1;
        self.core_1[u as usize] = v;
        self.core_2[v as usize] = u;

        // Update frontiers for directed graphs
        for e in self.g1.out_neighbors(u).iter() {
            let w = e.target as usize;
            if self.in_1[w] == 0  { self.in_1[w] = self.depth; }
            if self.out_1[w] == 0 { self.out_1[w] = self.depth; }
        }
        if self.g1.is_directed() {
            // Also track in-neighbors (nodes with edge to u)
            for n in self.g1.nodes() {
                if self.g1.out_neighbors(n).iter().any(|e| e.target == u) {
                    let w = n as usize;
                    if self.in_1[w] == 0  { self.in_1[w] = self.depth; }
                }
            }
        }
        for e in self.g2.out_neighbors(v).iter() {
            let w = e.target as usize;
            if self.in_2[w] == 0  { self.in_2[w] = self.depth; }
            if self.out_2[w] == 0 { self.out_2[w] = self.depth; }
        }
        if self.g2.is_directed() {
            for n in self.g2.nodes() {
                if self.g2.out_neighbors(n).iter().any(|e| e.target == v) {
                    let w = n as usize;
                    if self.in_2[w] == 0  { self.in_2[w] = self.depth; }
                }
            }
        }
    }

    fn remove_pair(&mut self, u: NodeId, v: NodeId) {
        self.core_1[u as usize] = UNMAP;
        self.core_2[v as usize] = UNMAP;

        // Remove frontier entries added at this depth
        for w in 0..self.n1 {
            if self.in_1[w]  == self.depth { self.in_1[w] = 0; }
            if self.out_1[w] == self.depth { self.out_1[w] = 0; }
        }
        for w in 0..self.n2 {
            if self.in_2[w]  == self.depth { self.in_2[w] = 0; }
            if self.out_2[w] == self.depth { self.out_2[w] = 0; }
        }
        self.depth -= 1;
    }

    fn is_feasible(&self, u: NodeId, v: NodeId, subgraph: bool) -> bool {
        // Structural check: neighbours of u in g1 must map consistently to neighbours of v in g2.
        let g1 = self.g1;
        let g2 = self.g2;

        // Check edges from u in g1 → mapped nodes must have edge in g2
        for e in g1.out_neighbors(u).iter() {
            let t = e.target;
            if self.core_1[t as usize] != UNMAP {
                let t2 = self.core_1[t as usize];
                if !g2.has_edge(v, t2) { return false; }
            }
        }
        // Check edges to u in g1 (reverse direction for directed)
        if g1.is_directed() {
            for n in g1.nodes() {
                if g1.out_neighbors(n).iter().any(|e| e.target == u) {
                    if self.core_1[n as usize] != UNMAP {
                        let n2 = self.core_1[n as usize];
                        if !g2.has_edge(n2, v) { return false; }
                    }
                }
            }
        }

        // Check reverse direction: edges from v in g2 to already-mapped nodes
        // must have a corresponding g1 edge (only for full isomorphism; subgraph
        // may have extra edges in g2 between mapped nodes)
        if !subgraph {
            for e in g2.out_neighbors(v).iter() {
                let t = e.target;
                if self.core_2[t as usize] != UNMAP {
                    let t1 = self.core_2[t as usize];
                    if !g1.has_edge(u, t1) { return false; }
                }
            }
            if g2.is_directed() {
                for n in g2.nodes() {
                    if g2.out_neighbors(n).iter().any(|e| e.target == v) {
                        if self.core_2[n as usize] != UNMAP {
                            let n1 = self.core_2[n as usize];
                            if !g1.has_edge(n1, u) { return false; }
                        }
                    }
                }
            }
        }

        // Lookahead pruning counters
        let (t1out, t1in, t2out, t2in, new1, new2) = self.lookahead_counts(u, v);

        if subgraph {
            // Subgraph: g1 frontier must be ≤ g2 frontier
            if t1out > t2out || t1in > t2in || new1 > new2 { return false; }
        } else {
            // Full isomorphism: frontiers must match exactly
            if t1out != t2out || t1in != t2in || new1 != new2 { return false; }
        }

        true
    }

    fn lookahead_counts(&self, u: NodeId, v: NodeId) -> (usize,usize,usize,usize,usize,usize) {
        let g1 = self.g1;
        let g2 = self.g2;

        let mut t1out = 0usize; let mut t1in = 0usize; let mut new1 = 0usize;
        for e in g1.out_neighbors(u).iter() {
            let w = e.target as usize;
            if self.core_1[w] == UNMAP {
                if self.out_1[w] > 0 { t1out += 1; }
                else if self.in_1[w] > 0 { t1in += 1; }
                else { new1 += 1; }
            }
        }

        let mut t2out = 0usize; let mut t2in = 0usize; let mut new2 = 0usize;
        for e in g2.out_neighbors(v).iter() {
            let w = e.target as usize;
            if self.core_2[w] == UNMAP {
                if self.out_2[w] > 0 { t2out += 1; }
                else if self.in_2[w] > 0 { t2in += 1; }
                else { new2 += 1; }
            }
        }

        (t1out, t1in, t2out, t2in, new1, new2)
    }

    /// Generate candidate pairs (u, v) for the next mapping step.
    fn candidates(&self, subgraph: bool) -> Vec<(NodeId, NodeId)> {
        // Pick one unmapped node from g1's frontier (smallest id for determinism)
        let u_opt: Option<NodeId> = {
            let out_frontier = self.nodes_1.iter().copied()
                .filter(|&n| self.core_1[n as usize] == UNMAP && self.out_1[n as usize] > 0)
                .min();
            let in_frontier = self.nodes_1.iter().copied()
                .filter(|&n| self.core_1[n as usize] == UNMAP && self.in_1[n as usize] > 0)
                .min();
            out_frontier
                .or(in_frontier)
                .or_else(|| self.nodes_1.iter().copied()
                    .filter(|&n| self.core_1[n as usize] == UNMAP).next())
        };
        let u = match u_opt { Some(u) => u, None => return vec![] };

        // For the v pool: use g2 frontier when non-empty; for subgraph use all unmapped g2 nodes
        let v_pool: Vec<NodeId> = if subgraph {
            // In subgraph mode, any unmapped node in g2 is a candidate
            let out_2: Vec<NodeId> = self.nodes_2.iter().copied()
                .filter(|&n| self.core_2[n as usize] == UNMAP && self.out_2[n as usize] > 0)
                .collect();
            let in_2: Vec<NodeId> = self.nodes_2.iter().copied()
                .filter(|&n| self.core_2[n as usize] == UNMAP && self.in_2[n as usize] > 0)
                .collect();
            if !out_2.is_empty() { out_2 }
            else if !in_2.is_empty() { in_2 }
            else {
                self.nodes_2.iter().copied()
                    .filter(|&n| self.core_2[n as usize] == UNMAP)
                    .collect()
            }
        } else {
            let out_2: Vec<NodeId> = self.nodes_2.iter().copied()
                .filter(|&n| self.core_2[n as usize] == UNMAP && self.out_2[n as usize] > 0)
                .collect();
            let in_2: Vec<NodeId> = self.nodes_2.iter().copied()
                .filter(|&n| self.core_2[n as usize] == UNMAP && self.in_2[n as usize] > 0)
                .collect();
            if !out_2.is_empty() { out_2 }
            else if !in_2.is_empty() { in_2 }
            else {
                self.nodes_2.iter().copied()
                    .filter(|&n| self.core_2[n as usize] == UNMAP)
                    .collect()
            }
        };

        v_pool.into_iter().map(|v| (u, v)).collect()
    }

    fn current_mapping(&self) -> HashMap<NodeId, NodeId> {
        self.nodes_1.iter().copied()
            .filter(|&n| self.core_1[n as usize] != UNMAP)
            .map(|n| (n, self.core_1[n as usize]))
            .collect()
    }

    fn is_complete(&self) -> bool {
        self.nodes_1.iter().all(|&n| self.core_1[n as usize] != UNMAP)
    }
}

// ─── Core recursive match ─────────────────────────────────────────────────────

fn vf2_match(
    state: &mut Vf2State<'_>,
    subgraph: bool,
    all: bool,
    results: &mut Vec<HashMap<NodeId, NodeId>>,
    limit: usize,
) -> bool {
    if results.len() >= limit { return true; }

    if state.is_complete() {
        results.push(state.current_mapping());
        return !all || results.len() >= limit;
    }

    let candidates = state.candidates(subgraph);
    for (u, v) in candidates {
        if state.is_feasible(u, v, subgraph) {
            state.add_pair(u, v);
            if vf2_match(state, subgraph, all, results, limit) {
                state.remove_pair(u, v);
                return true;
            }
            state.remove_pair(u, v);
        }
    }
    false
}

// ─── Public API ───────────────────────────────────────────────────────────────

/// Return `true` if `g1` and `g2` are isomorphic (structurally identical).
///
/// Uses the VF2 algorithm.  Node/edge labels are not considered — only
/// the graph structure (adjacency) is checked.
///
/// Runtime: O(n!) worst case, O(n + m) typical on sparse graphs.
pub fn is_isomorphic(g1: &Graph, g2: &Graph) -> bool {
    // Quick size check
    if g1.node_count() != g2.node_count() || g1.edge_count() != g2.edge_count() {
        return false;
    }
    if g1.node_count() == 0 { return true; }

    let mut state = Vf2State::new(g1, g2);
    let mut results = Vec::new();
    vf2_match(&mut state, false, false, &mut results, 1);
    !results.is_empty()
}

/// Find one isomorphism mapping from `g1` to `g2`.
///
/// Returns `Some(mapping)` where `mapping[u] = v` means node `u` in g1
/// corresponds to node `v` in g2.  Returns `None` if no isomorphism exists.
pub fn vf2_mapping(g1: &Graph, g2: &Graph) -> Option<HashMap<NodeId, NodeId>> {
    if g1.node_count() != g2.node_count() || g1.edge_count() != g2.edge_count() {
        return None;
    }
    if g1.node_count() == 0 { return Some(HashMap::new()); }

    let mut state = Vf2State::new(g1, g2);
    let mut results = Vec::new();
    vf2_match(&mut state, false, false, &mut results, 1);
    results.into_iter().next()
}

/// Return all isomorphism mappings from `g1` to `g2` (up to `limit`).
///
/// If `limit` is `None`, all mappings are enumerated (may be very large for
/// highly symmetric graphs like complete graphs).
pub fn vf2_all_mappings(
    g1: &Graph,
    g2: &Graph,
    limit: Option<usize>,
) -> Vec<HashMap<NodeId, NodeId>> {
    if g1.node_count() != g2.node_count() || g1.edge_count() != g2.edge_count() {
        return vec![];
    }
    if g1.node_count() == 0 { return vec![HashMap::new()]; }

    let cap = limit.unwrap_or(usize::MAX);
    let mut state = Vf2State::new(g1, g2);
    let mut results = Vec::new();
    vf2_match(&mut state, false, true, &mut results, cap);
    results
}

/// Return `true` if `pattern` is isomorphic to some subgraph of `graph`.
///
/// A subgraph may contain any subset of the nodes of `graph` and all edges
/// between those nodes that exist in `graph` (induced subgraph).
pub fn is_subgraph_isomorphic(graph: &Graph, pattern: &Graph) -> bool {
    if pattern.node_count() > graph.node_count() { return false; }
    if pattern.node_count() == 0 { return true; }

    let mut state = Vf2State::new(pattern, graph);
    let mut results = Vec::new();
    vf2_match(&mut state, true, false, &mut results, 1);
    !results.is_empty()
}

/// Find one subgraph isomorphism mapping from `pattern` into `graph`.
///
/// Returns `Some(mapping)` where `mapping[p] = g` means node `p` in pattern
/// maps to node `g` in graph.
pub fn subgraph_vf2_mapping(
    graph: &Graph,
    pattern: &Graph,
) -> Option<HashMap<NodeId, NodeId>> {
    if pattern.node_count() > graph.node_count() { return None; }
    if pattern.node_count() == 0 { return Some(HashMap::new()); }

    let mut state = Vf2State::new(pattern, graph);
    let mut results = Vec::new();
    vf2_match(&mut state, true, false, &mut results, 1);
    results.into_iter().next()
}

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

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

    fn path(n: usize) -> Graph {
        let mut g = Graph::new(GraphConfig::simple());
        for _ in 0..n { g.add_node(); }
        for i in 0..(n as NodeId - 1) { g.add_edge(i, i+1, None); }
        g
    }

    fn cycle(n: usize) -> Graph {
        let mut g = Graph::new(GraphConfig::simple());
        for _ in 0..n { g.add_node(); }
        for i in 0..n as NodeId { g.add_edge(i, (i+1) % n as NodeId, None); }
        g
    }

    fn complete(n: usize) -> Graph {
        let mut g = Graph::new(GraphConfig::simple());
        for _ in 0..n { g.add_node(); }
        for i in 0..n as NodeId { for j in (i+1)..n as NodeId { g.add_edge(i, j, None); } }
        g
    }

    fn star(n: usize) -> Graph {
        let mut g = Graph::new(GraphConfig::simple());
        for _ in 0..n { g.add_node(); }
        for i in 1..n as NodeId { g.add_edge(0, i, None); }
        g
    }

    // ── is_isomorphic ─────────────────────────────────────────────────────────

    #[test]
    fn test_iso_paths_same_length() {
        assert!(is_isomorphic(&path(4), &path(4)));
    }

    #[test]
    fn test_iso_paths_different_length() {
        assert!(!is_isomorphic(&path(3), &path(4)));
    }

    #[test]
    fn test_iso_cycle_vs_path() {
        // C4 ≇ P4 (different edge counts)
        assert!(!is_isomorphic(&cycle(4), &path(4)));
    }

    #[test]
    fn test_iso_k3_vs_c3() {
        // K3 = C3 (triangle)
        assert!(is_isomorphic(&complete(3), &cycle(3)));
    }

    #[test]
    fn test_iso_k4_vs_k4() {
        assert!(is_isomorphic(&complete(4), &complete(4)));
    }

    #[test]
    fn test_iso_star_vs_path() {
        // S4 (star with 4 leaves) ≇ P5
        assert!(!is_isomorphic(&star(5), &path(5)));
    }

    #[test]
    fn test_iso_empty_graphs() {
        let g1 = Graph::new(GraphConfig::simple());
        let g2 = Graph::new(GraphConfig::simple());
        assert!(is_isomorphic(&g1, &g2));
    }

    #[test]
    fn test_iso_single_node() {
        let mut g1 = Graph::new(GraphConfig::simple()); g1.add_node();
        let mut g2 = Graph::new(GraphConfig::simple()); g2.add_node();
        assert!(is_isomorphic(&g1, &g2));
    }

    #[test]
    fn test_iso_directed_cycle() {
        // Two directed 3-cycles are isomorphic
        let mut g1 = Graph::new(GraphConfig::directed());
        let mut g2 = Graph::new(GraphConfig::directed());
        for _ in 0..3 { g1.add_node(); g2.add_node(); }
        g1.add_edge(0,1,None); g1.add_edge(1,2,None); g1.add_edge(2,0,None);
        g2.add_edge(0,2,None); g2.add_edge(2,1,None); g2.add_edge(1,0,None);
        assert!(is_isomorphic(&g1, &g2));
    }

    // ── vf2_mapping ───────────────────────────────────────────────────────────

    #[test]
    fn test_vf2_mapping_found() {
        let g1 = path(3);
        let g2 = path(3);
        let m = vf2_mapping(&g1, &g2).expect("isomorphic paths must have a mapping");
        // Verify mapping preserves edges
        for u in g1.nodes() {
            for e in g1.out_neighbors(u).iter() {
                let mu = m[&u];
                let mv = m[&e.target];
                assert!(g2.has_edge(mu, mv) || g2.has_edge(mv, mu),
                    "mapped edge ({mu},{mv}) must exist in g2");
            }
        }
    }

    #[test]
    fn test_vf2_mapping_none() {
        assert!(vf2_mapping(&path(3), &path(4)).is_none());
    }

    // ── is_subgraph_isomorphic ────────────────────────────────────────────────

    #[test]
    fn test_subgraph_path_in_complete() {
        // P3 is a subgraph of a path P5 (not induced in K4 since K4 has extra edges)
        let p5 = path(5);
        let p3 = path(3);
        assert!(is_subgraph_isomorphic(&p5, &p3),
            "P3 should be an induced subgraph of P5");
        // Also: cycle(4) contains P3 as a subgraph
        let c4 = cycle(4);
        assert!(is_subgraph_isomorphic(&c4, &p3),
            "P3 should be an induced subgraph of C4");
    }

    #[test]
    fn test_subgraph_k3_in_k4() {
        // K3 is a subgraph of K4
        assert!(is_subgraph_isomorphic(&complete(4), &complete(3)));
    }

    #[test]
    fn test_subgraph_k4_not_in_k3() {
        assert!(!is_subgraph_isomorphic(&complete(3), &complete(4)));
    }

    #[test]
    fn test_subgraph_star_in_k5() {
        // Star with 3 leaves ⊆ K5
        assert!(is_subgraph_isomorphic(&complete(5), &star(4)));
    }

    #[test]
    fn test_subgraph_empty_pattern() {
        // Empty pattern matches anything
        let empty = Graph::new(GraphConfig::simple());
        assert!(is_subgraph_isomorphic(&complete(4), &empty));
    }

    // ── vf2_all_mappings ──────────────────────────────────────────────────────

    #[test]
    fn test_all_mappings_k2() {
        // K2 has exactly 2 automorphisms (swap the two nodes)
        let k2 = complete(2);
        let maps = vf2_all_mappings(&k2, &k2, None);
        assert_eq!(maps.len(), 2, "K2 has 2 automorphisms, got {}", maps.len());
    }

    #[test]
    fn test_all_mappings_limit() {
        let k4 = complete(4);
        let maps = vf2_all_mappings(&k4, &k4, Some(5));
        assert!(maps.len() <= 5);
    }

    #[test]
    fn test_all_mappings_none_exist() {
        let maps = vf2_all_mappings(&path(3), &path(4), None);
        assert!(maps.is_empty());
    }
}