"""
Routines for traversing graphs in compressed sparse format
"""

# Author: Jake Vanderplas  -- <vanderplas@astro.washington.edu>
# License: BSD, (C) 2012

import numpy as np
cimport numpy as np

from scipy.sparse.csgraph._validation import validate_graph
from scipy.sparse.csgraph._tools import reconstruct_path

from libcpp.vector cimport vector
cimport cython

np.import_array()

include 'parameters.pxi'

def connected_components(csgraph, directed=True, connection='weak',
                         return_labels=True):
    """
    connected_components(csgraph, directed=True, connection='weak',
                         return_labels=True)

    Analyze the connected components of a sparse graph.

    .. versionadded:: 0.11.0

    Parameters
    ----------
    csgraph : array_like or sparse array or matrix
        The N x N matrix representing the compressed sparse graph.  The input
        csgraph will be converted to csr format for the calculation.
    directed : bool, optional
        If True (default), then operate on a directed graph: only
        move from point i to point j along paths csgraph[i, j].
        If False, then find the shortest path on an undirected graph: the
        algorithm can progress from point i to j along csgraph[i, j] or
        csgraph[j, i].
    connection : str, optional
        ['weak'|'strong'].  For directed graphs, the type of connection to
        use.  Nodes i and j are strongly connected if a path exists both
        from i to j and from j to i. A directed graph is weakly connected
        if replacing all of its directed edges with undirected edges produces
        a connected (undirected) graph. If directed == False, this keyword
        is not referenced.
    return_labels : bool, optional
        If True (default), then return the labels for each of the connected
        components.

    Returns
    -------
    n_components: int
        The number of connected components.
    labels: ndarray
        The length-N array of labels of the connected components.

    References
    ----------
    .. [1] Robert E. Tarjan and Uri Zwick, "Finding strong components using
           depth-first search", European Journal of Combinatorics, 119,
           2024, :doi:`10.1016/j.ejc.2023.103815`

    Examples
    --------
    >>> from scipy.sparse import csr_array
    >>> from scipy.sparse.csgraph import connected_components

    >>> graph = [
    ... [0, 1, 1, 0, 0],
    ... [0, 0, 1, 0, 0],
    ... [0, 0, 0, 0, 0],
    ... [0, 0, 0, 0, 1],
    ... [0, 0, 0, 0, 0]
    ... ]
    >>> graph = csr_array(graph)
    >>> print(graph)
    <Compressed Sparse Row sparse array of dtype 'int64'
        with 4 stored elements and shape (5, 5)>
        Coords	Values
        (0, 1)	1
        (0, 2)	1
        (1, 2)	1
        (3, 4)	1

    >>> n_components, labels = connected_components(csgraph=graph, directed=False, return_labels=True)
    >>> n_components
    2
    >>> labels
    array([0, 0, 0, 1, 1], dtype=int32)

    """
    if connection.lower() not in ['weak', 'strong']:
        raise ValueError("connection must be 'weak' or 'strong'")

    # weak connections <=> components of undirected graph
    if connection.lower() == 'weak':
        directed = False

    csgraph = validate_graph(csgraph, directed,
                             dtype=csgraph.dtype,
                             dense_output=False)

    labels = np.empty(csgraph.shape[0], dtype=ITYPE)

    if directed:
        n_components = _connected_components_directed(csgraph.indices,
                                                      csgraph.indptr,
                                                      labels)
    else:
        csgraph_T = csgraph.T.tocsr()
        n_components = _connected_components_undirected(csgraph.indices,
                                                        csgraph.indptr,
                                                        csgraph_T.indices,
                                                        csgraph_T.indptr,
                                                        labels)

    if return_labels:
        return n_components, labels
    else:
        return n_components


def breadth_first_tree(csgraph, i_start, directed=True):
    r"""
    breadth_first_tree(csgraph, i_start, directed=True)

    Return the tree generated by a breadth-first search.

    Note that a breadth-first tree from a specified node is unique.

    .. versionadded:: 0.11.0

    Parameters
    ----------
    csgraph : array_like or sparse array or matrix
        The N x N matrix representing the compressed sparse graph.  The input
        csgraph will be converted to csr format for the calculation.
    i_start : int
        The index of starting node.
    directed : bool, optional
        If True (default), then operate on a directed graph: only
        move from point i to point j along paths csgraph[i, j].
        If False, then find the shortest path on an undirected graph: the
        algorithm can progress from point i to j along csgraph[i, j] or
        csgraph[j, i].

    Returns
    -------
    cstree : csr matrix
        The N x N directed compressed-sparse representation of the breadth-
        first tree drawn from csgraph, starting at the specified node.

    Notes
    -----
    If multiple valid solutions are possible, output may vary with SciPy and
    Python version.

    Examples
    --------
    The following example shows the computation of a depth-first tree
    over a simple four-component graph, starting at node 0::

         input graph          breadth first tree from (0)

             (0)                         (0)
            /   \                       /   \
           3     8                     3     8
          /       \                   /       \
        (3)---5---(1)               (3)       (1)
          \       /                           /
           6     2                           2
            \   /                           /
             (2)                         (2)

    In compressed sparse representation, the solution looks like this:

    >>> from scipy.sparse import csr_array
    >>> from scipy.sparse.csgraph import breadth_first_tree
    >>> X = csr_array([[0, 8, 0, 3],
    ...                [0, 0, 2, 5],
    ...                [0, 0, 0, 6],
    ...                [0, 0, 0, 0]])
    >>> Tcsr = breadth_first_tree(X, 0, directed=False)
    >>> Tcsr.toarray().astype(int)
    array([[0, 8, 0, 3],
           [0, 0, 2, 0],
           [0, 0, 0, 0],
           [0, 0, 0, 0]])

    Note that the resulting graph is a Directed Acyclic Graph which spans
    the graph.  A breadth-first tree from a given node is unique.
    """
    _node_list, predecessors = breadth_first_order(csgraph, i_start,
                                                   directed, True)
    return reconstruct_path(csgraph, predecessors, directed)


def depth_first_tree(csgraph, i_start, directed=True):
    r"""
    depth_first_tree(csgraph, i_start, directed=True)

    Return a tree generated by a depth-first search.

    Note that a tree generated by a depth-first search is not unique:
    it depends on the order that the children of each node are searched.

    .. versionadded:: 0.11.0

    Parameters
    ----------
    csgraph : array_like or sparse array or matrix
        The N x N matrix representing the compressed sparse graph.  The input
        csgraph will be converted to csr format for the calculation.
    i_start : int
        The index of starting node.
    directed : bool, optional
        If True (default), then operate on a directed graph: only
        move from point i to point j along paths csgraph[i, j].
        If False, then find the shortest path on an undirected graph: the
        algorithm can progress from point i to j along csgraph[i, j] or
        csgraph[j, i].

    Returns
    -------
    cstree : csr matrix
        The N x N directed compressed-sparse representation of the depth-
        first tree drawn from csgraph, starting at the specified node.

    Notes
    -----
    If multiple valid solutions are possible, output may vary with SciPy and
    Python version.

    Examples
    --------
    The following example shows the computation of a depth-first tree
    over a simple four-component graph, starting at node 0::

         input graph           depth first tree from (0)

             (0)                         (0)
            /   \                           \
           3     8                           8
          /       \                           \
        (3)---5---(1)               (3)       (1)
          \       /                   \       /
           6     2                     6     2
            \   /                       \   /
             (2)                         (2)

    In compressed sparse representation, the solution looks like this:

    >>> from scipy.sparse import csr_array
    >>> from scipy.sparse.csgraph import depth_first_tree
    >>> X = csr_array([[0, 8, 0, 3],
    ...                [0, 0, 2, 5],
    ...                [0, 0, 0, 6],
    ...                [0, 0, 0, 0]])
    >>> Tcsr = depth_first_tree(X, 0, directed=False)
    >>> Tcsr.toarray().astype(int)
    array([[0, 8, 0, 0],
           [0, 0, 2, 0],
           [0, 0, 0, 6],
           [0, 0, 0, 0]])

    Note that the resulting graph is a Directed Acyclic Graph which spans
    the graph.  Unlike a breadth-first tree, a depth-first tree of a given
    graph is not unique if the graph contains cycles.  If the above solution
    had begun with the edge connecting nodes 0 and 3, the result would have
    been different.
    """
    _node_list, predecessors = depth_first_order(csgraph, i_start,
                                                 directed, True)
    return reconstruct_path(csgraph, predecessors, directed)


cpdef breadth_first_order(csgraph, i_start,
                          directed=True, return_predecessors=True):
    """
    breadth_first_order(csgraph, i_start, directed=True, return_predecessors=True)

    Return a breadth-first ordering starting with specified node.

    Note that a breadth-first order is not unique, but the tree which it
    generates is unique.

    .. versionadded:: 0.11.0

    Parameters
    ----------
    csgraph : array_like or sparse array or matrix
        The N x N compressed sparse graph.  The input csgraph will be
        converted to csr format for the calculation.
    i_start : int
        The index of starting node.
    directed : bool, optional
        If True (default), then operate on a directed graph: only
        move from point i to point j along paths csgraph[i, j].
        If False, then find the shortest path on an undirected graph: the
        algorithm can progress from point i to j along csgraph[i, j] or
        csgraph[j, i].
    return_predecessors : bool, optional
        If True (default), then return the predecessor array (see below).

    Returns
    -------
    node_array : ndarray, one dimension
        The breadth-first list of nodes, starting with specified node.  The
        length of node_array is the number of nodes reachable from the
        specified node.
    predecessors : ndarray, one dimension
        Returned only if return_predecessors is True.
        The length-N list of predecessors of each node in a breadth-first
        tree.  If node i is in the tree, then its parent is given by
        predecessors[i]. If node i is not in the tree (and for the parent
        node) then predecessors[i] = -9999.

    Notes
    -----
    If multiple valid solutions are possible, output may vary with SciPy and
    Python version.

    Examples
    --------
    >>> from scipy.sparse import csr_array
    >>> from scipy.sparse.csgraph import breadth_first_order

    >>> graph = [
    ... [0, 1, 2, 0],
    ... [0, 0, 0, 1],
    ... [2, 0, 0, 3],
    ... [0, 0, 0, 0]
    ... ]
    >>> graph = csr_array(graph)
    >>> print(graph)
    <Compressed Sparse Row sparse array of dtype 'int64'
        with 5 stored elements and shape (4, 4)>
        Coords	Values
        (0, 1)	1
        (0, 2)	2
        (1, 3)	1
        (2, 0)	2
        (2, 3)	3

    >>> breadth_first_order(graph,0)
    (array([0, 1, 2, 3], dtype=int32), array([-9999,     0,     0,     1], dtype=int32))

    """
    csgraph = validate_graph(csgraph, directed, dense_output=False)
    cdef int N = csgraph.shape[0]

    cdef np.ndarray node_list = np.empty(N, dtype=ITYPE)
    cdef np.ndarray predecessors = np.empty(N, dtype=ITYPE)
    node_list.fill(NULL_IDX)
    predecessors.fill(NULL_IDX)

    if directed:
        length = _breadth_first_directed(i_start,
                                csgraph.indices, csgraph.indptr,
                                node_list, predecessors)
    else:
        csgraph_T = csgraph.T.tocsr()
        length = _breadth_first_undirected(i_start,
                                           csgraph.indices, csgraph.indptr,
                                           csgraph_T.indices, csgraph_T.indptr,
                                           node_list, predecessors)

    if return_predecessors:
        return node_list[:length], predecessors
    else:
        return node_list[:length]


def _breadth_first_directed(
        unsigned int head_node,
        np.ndarray[int32_or_int64, ndim=1, mode='c'] indices,
        np.ndarray[int32_or_int64, ndim=1, mode='c'] indptr,
        np.ndarray[ITYPE_t, ndim=1, mode='c'] node_list,
        np.ndarray[ITYPE_t, ndim=1, mode='c'] predecessors):
    return _breadth_first_directed2(head_node, indices, indptr,
                                    node_list, predecessors)


cdef unsigned int _breadth_first_directed2(
        unsigned int head_node,
        np.ndarray[int32_or_int64, ndim=1, mode='c'] indices,
        np.ndarray[int32_or_int64, ndim=1, mode='c'] indptr,
        np.ndarray[ITYPE_t, ndim=1, mode='c'] node_list,
        np.ndarray[ITYPE_t, ndim=1, mode='c'] predecessors) noexcept:
    # Inputs:
    #  head_node: (input) index of the node from which traversal starts
    #  indices: (input) CSR indices of graph
    #  indptr:  (input) CSR indptr of graph
    #  node_list: (output) breadth-first list of nodes
    #  predecessors: (output) list of predecessors of nodes in breadth-first
    #                tree.  Should be initialized to NULL_IDX
    # Returns:
    #  n_nodes: the number of nodes in the breadth-first tree
    cdef Py_ssize_t i
    cdef unsigned int pnode, cnode
    cdef unsigned int i_nl, i_nl_end

    node_list[0] = head_node
    i_nl = 0
    i_nl_end = 1

    while i_nl < i_nl_end:
        pnode = node_list[i_nl]

        for i in range(indptr[pnode], indptr[pnode + 1]):
            cnode = indices[i]
            if (cnode == head_node):
                continue
            elif (predecessors[cnode] == NULL_IDX):
                node_list[i_nl_end] = cnode
                predecessors[cnode] = pnode
                i_nl_end += 1

        i_nl += 1

    return i_nl

def _breadth_first_undirected(
        unsigned int head_node,
        np.ndarray[int32_or_int64, ndim=1, mode='c'] indices1,
        np.ndarray[int32_or_int64, ndim=1, mode='c'] indptr1,
        np.ndarray[int32_or_int64_b, ndim=1, mode='c'] indices2,
        np.ndarray[int32_or_int64_b, ndim=1, mode='c'] indptr2,
        np.ndarray[ITYPE_t, ndim=1, mode='c'] node_list,
        np.ndarray[ITYPE_t, ndim=1, mode='c'] predecessors):
    return _breadth_first_undirected2(head_node, indices1, indptr1, indices2,
                                      indptr2, node_list, predecessors)

cdef unsigned int _breadth_first_undirected2(
        unsigned int head_node,
        np.ndarray[int32_or_int64, ndim=1, mode='c'] indices1,
        np.ndarray[int32_or_int64, ndim=1, mode='c'] indptr1,
        np.ndarray[int32_or_int64_b, ndim=1, mode='c'] indices2,
        np.ndarray[int32_or_int64_b, ndim=1, mode='c'] indptr2,
        np.ndarray[ITYPE_t, ndim=1, mode='c'] node_list,
        np.ndarray[ITYPE_t, ndim=1, mode='c'] predecessors) noexcept:
    # Inputs:
    #  head_node: (input) index of the node from which traversal starts
    #  indices1: (input) CSR indices of graph
    #  indptr1:  (input) CSR indptr of graph
    #  indices2: (input) CSR indices of transposed graph
    #  indptr2:  (input) CSR indptr of transposed graph
    #  node_list: (output) breadth-first list of nodes
    #  predecessors: (output) list of predecessors of nodes in breadth-first
    #                tree.  Should be initialized to NULL_IDX
    # Returns:
    #  n_nodes: the number of nodes in the breadth-first tree
    cdef Py_ssize_t i
    cdef unsigned int pnode, cnode
    cdef unsigned int i_nl, i_nl_end

    node_list[0] = head_node
    i_nl = 0
    i_nl_end = 1

    while i_nl < i_nl_end:
        pnode = node_list[i_nl]

        for i in range(indptr1[pnode], indptr1[pnode + 1]):
            cnode = indices1[i]
            if (cnode == head_node):
                continue
            elif (predecessors[cnode] == NULL_IDX):
                node_list[i_nl_end] = cnode
                predecessors[cnode] = pnode
                i_nl_end += 1

        for i in range(indptr2[pnode], indptr2[pnode + 1]):
            cnode = indices2[i]
            if (cnode == head_node):
                continue
            elif (predecessors[cnode] == NULL_IDX):
                node_list[i_nl_end] = cnode
                predecessors[cnode] = pnode
                i_nl_end += 1

        i_nl += 1

    return i_nl


cpdef depth_first_order(csgraph, i_start,
                        directed=True, return_predecessors=True):
    """
    depth_first_order(csgraph, i_start, directed=True, return_predecessors=True)

    Return a depth-first ordering starting with specified node.

    Note that a depth-first order is not unique.  Furthermore, for graphs
    with cycles, the tree generated by a depth-first search is not
    unique either.

    .. versionadded:: 0.11.0

    Parameters
    ----------
    csgraph : array_like or sparse array or matrix
        The N x N compressed sparse graph.  The input csgraph will be
        converted to csr format for the calculation.
    i_start : int
        The index of starting node.
    directed : bool, optional
        If True (default), then operate on a directed graph: only
        move from point i to point j along paths csgraph[i, j].
        If False, then find the shortest path on an undirected graph: the
        algorithm can progress from point i to j along csgraph[i, j] or
        csgraph[j, i].
    return_predecessors : bool, optional
        If True (default), then return the predecessor array (see below).

    Returns
    -------
    node_array : ndarray, one dimension
        The depth-first list of nodes, starting with specified node.  The
        length of node_array is the number of nodes reachable from the
        specified node.
    predecessors : ndarray, one dimension
        Returned only if return_predecessors is True.
        The length-N list of predecessors of each node in a depth-first
        tree.  If node i is in the tree, then its parent is given by
        predecessors[i]. If node i is not in the tree (and for the parent
        node) then predecessors[i] = -9999.

    Notes
    -----
    If multiple valid solutions are possible, output may vary with SciPy and
    Python version.

    Examples
    --------
    >>> from scipy.sparse import csr_array
    >>> from scipy.sparse.csgraph import depth_first_order

    >>> graph = [
    ... [0, 1, 2, 0],
    ... [0, 0, 0, 1],
    ... [2, 0, 0, 3],
    ... [0, 0, 0, 0]
    ... ]
    >>> graph = csr_array(graph)
    >>> print(graph)
    <Compressed Sparse Row sparse array of dtype 'int64'
        with 5 stored elements and shape (4, 4)>
        Coords	Values
        (0, 1)	1
        (0, 2)	2
        (1, 3)	1
        (2, 0)	2
        (2, 3)	3

    >>> depth_first_order(graph,0)
    (array([0, 1, 3, 2], dtype=int32), array([-9999,     0,     0,     1], dtype=int32))

    """
    csgraph = validate_graph(csgraph, directed, dense_output=False)
    cdef int N = csgraph.shape[0]

    node_list = np.empty(N, dtype=ITYPE)
    predecessors = np.empty(N, dtype=ITYPE)
    root_list = np.empty(N, dtype=ITYPE)
    flag = np.zeros(N, dtype=ITYPE)
    node_list.fill(NULL_IDX)
    predecessors.fill(NULL_IDX)
    root_list.fill(NULL_IDX)

    if directed:
        length = _depth_first_directed(i_start,
                              csgraph.indices, csgraph.indptr,
                              node_list, predecessors,
                              root_list, flag)
    else:
        csgraph_T = csgraph.T.tocsr()
        length = _depth_first_undirected(i_start,
                                         csgraph.indices, csgraph.indptr,
                                         csgraph_T.indices, csgraph_T.indptr,
                                         node_list, predecessors,
                                         root_list, flag)

    if return_predecessors:
        return node_list[:length], predecessors
    else:
        return node_list[:length]


def _depth_first_directed(
        unsigned int head_node,
        np.ndarray[int32_or_int64, ndim=1, mode='c'] indices,
        np.ndarray[int32_or_int64, ndim=1, mode='c'] indptr,
        np.ndarray[ITYPE_t, ndim=1, mode='c'] node_list,
        np.ndarray[ITYPE_t, ndim=1, mode='c'] predecessors,
        np.ndarray[ITYPE_t, ndim=1, mode='c'] root_list,
        np.ndarray[ITYPE_t, ndim=1, mode='c'] flag):
    return _depth_first_directed2(head_node, indices, indptr,
                                  node_list, predecessors,
                                  root_list, flag)


cdef unsigned int _depth_first_directed2(
        unsigned int head_node,
        np.ndarray[int32_or_int64, ndim=1, mode='c'] indices,
        np.ndarray[int32_or_int64, ndim=1, mode='c'] indptr,
        np.ndarray[ITYPE_t, ndim=1, mode='c'] node_list,
        np.ndarray[ITYPE_t, ndim=1, mode='c'] predecessors,
        np.ndarray[ITYPE_t, ndim=1, mode='c'] root_list,
        np.ndarray[ITYPE_t, ndim=1, mode='c'] flag) noexcept:
    cdef Py_ssize_t i
    cdef unsigned int i_nl_end, cnode, pnode
    cdef unsigned int N = node_list.shape[0]
    cdef int no_children, i_root

    node_list[0] = head_node
    root_list[0] = head_node
    i_root = 0
    i_nl_end = 1
    flag[head_node] = 1

    while i_root >= 0:
        pnode = root_list[i_root]
        no_children = True
        for i in range(indptr[pnode], indptr[pnode + 1]):
            cnode = indices[i]
            if flag[cnode]:
                continue
            else:
                i_root += 1
                root_list[i_root] = cnode
                node_list[i_nl_end] = cnode
                predecessors[cnode] = pnode
                flag[cnode] = 1
                i_nl_end += 1
                no_children = False
                break

        if i_nl_end == N:
            break

        if no_children:
            i_root -= 1

    return i_nl_end


def _depth_first_undirected(
        unsigned int head_node,
        np.ndarray[int32_or_int64, ndim=1, mode='c'] indices1,
        np.ndarray[int32_or_int64, ndim=1, mode='c'] indptr1,
        np.ndarray[int32_or_int64_b, ndim=1, mode='c'] indices2,
        np.ndarray[int32_or_int64_b, ndim=1, mode='c'] indptr2,
        np.ndarray[ITYPE_t, ndim=1, mode='c'] node_list,
        np.ndarray[ITYPE_t, ndim=1, mode='c'] predecessors,
        np.ndarray[ITYPE_t, ndim=1, mode='c'] root_list,
        np.ndarray[ITYPE_t, ndim=1, mode='c'] flag):
    return _depth_first_undirected2(head_node, indices1, indptr1,
                                    indices2, indptr2,
                                    node_list, predecessors,
                                    root_list, flag)


cdef unsigned int _depth_first_undirected2(
        unsigned int head_node,
        np.ndarray[int32_or_int64, ndim=1, mode='c'] indices1,
        np.ndarray[int32_or_int64, ndim=1, mode='c'] indptr1,
        np.ndarray[int32_or_int64_b, ndim=1, mode='c'] indices2,
        np.ndarray[int32_or_int64_b, ndim=1, mode='c'] indptr2,
        np.ndarray[ITYPE_t, ndim=1, mode='c'] node_list,
        np.ndarray[ITYPE_t, ndim=1, mode='c'] predecessors,
        np.ndarray[ITYPE_t, ndim=1, mode='c'] root_list,
        np.ndarray[ITYPE_t, ndim=1, mode='c'] flag) noexcept:
    cdef Py_ssize_t i
    cdef unsigned int i_nl_end, cnode, pnode
    cdef unsigned int N = node_list.shape[0]
    cdef int no_children, i_root

    node_list[0] = head_node
    root_list[0] = head_node
    i_root = 0
    i_nl_end = 1
    flag[head_node] = 1

    while i_root >= 0:
        pnode = root_list[i_root]
        no_children = True

        for i in range(indptr1[pnode], indptr1[pnode + 1]):
            cnode = indices1[i]
            if flag[cnode]:
                continue
            else:
                i_root += 1
                root_list[i_root] = cnode
                node_list[i_nl_end] = cnode
                predecessors[cnode] = pnode
                flag[cnode] = 1
                i_nl_end += 1
                no_children = False
                break

        if no_children:
            for i in range(indptr2[pnode], indptr2[pnode + 1]):
                cnode = indices2[i]
                if flag[cnode]:
                    continue
                else:
                    i_root += 1
                    root_list[i_root] = cnode
                    node_list[i_nl_end] = cnode
                    predecessors[cnode] = pnode
                    flag[cnode] = 1
                    i_nl_end += 1
                    no_children = False
                    break

        if i_nl_end == N:
            break

        if no_children:
            i_root -= 1

    return i_nl_end


cdef inline int _dfs_node(int v) noexcept nogil:
    """Decode a DFS stack entry to a node index (clear the sign/lead bit)."""
    return v & 0x7FFFFFFF


# Author: Sebastiano Vigna  -- <sebastiano.vigna@gmail.com>
def _connected_components_directed(
        np.ndarray[int32_or_int64, ndim=1, mode='c'] indices,
        np.ndarray[int32_or_int64, ndim=1, mode='c'] indptr,
        np.ndarray[ITYPE_t, ndim=1, mode='c'] labels):
    return _connected_components_directed2(indices, indptr, labels)


@cython.boundscheck(False)
@cython.wraparound(False)
cdef int _connected_components_directed2(
        np.ndarray[int32_or_int64, ndim=1, mode='c'] indices,
        np.ndarray[int32_or_int64, ndim=1, mode='c'] indptr,
        np.ndarray[ITYPE_t, ndim=1, mode='c'] labels) noexcept:
    """
    Uses an iterative version of Tarjan's algorithm to find the
    strongly connected components of a directed graph represented as a
    CSR sparse array (scipy.sparse.csr_array).

    The algorithmic complexity for a graph with E edges and V vertices
    is O(E + V). The storage requirement is an array of V elements
    (int32 or int64, matching the CSR index type) tracking successor
    positions, plus two int32 stacks whose combined length never exceeds
    V, and is much smaller in practice.

    The algorithm uses all improvements described by Tarjan and Zwick in
    their recent survey [1]_, plus some further tricks used in the Rust
    implementation of the WebGraph framework [2]_.

    References
    ----------
    .. [1] Robert E. Tarjan and Uri Zwick, "Finding strong components using
           depth-first search", European Journal of Combinatorics, 119,
           2024, :doi:`10.1016/j.ejc.2023.103815`
    .. [2] Tommaso Fontana, Sebastiano Vigna, and Stefano Zacchiroli,
           "WebGraph: The next generation (is in Rust)", Companion
           Proceedings of the ACM Web Conference 2024, pp. 686--689, 2024.
    """
    DEF VOID = -1
    cdef int N = labels.shape[0]
    if N == 0:
        return 0

    cdef int v, w, j, parent, top
    cdef int NONLEAD = <int>0x80000000  # sign bit: marks stack entry as non-lead
    cdef int index = N        # timestamp counter, decreasing
    cdef int n_comp = 0       # number of emitted components
    cdef int root_hl = 0      # high_link of current DFS-tree root
    cdef bint done = False    # set True on early exit to break outer loop

    # Iterative Tarjan's algorithm using reverse timestamps and
    # encoding a bit stack of lead nodes in the sign bit of the DFS stack.
    #
    # Timestamps decrease from N to 1 for discovered nodes.  Component
    # numbers increase from 0 for emitted SCCs.  Unvisited nodes have
    # high_link = 0, distinguished from component 0 via succ_pos == VOID.
    # labels doubles as the high_link array.
    #
    # Decreasing timestamps make renumbering at the end unnecessary. The
    # "lead" flag is materialized in an array in Tarjan & Zwick's survey,
    # but a stack parallel to the DFS stack is sufficient.
    #
    # succ_pos[v]: position in successor list for node v (int32 or int64,
    #     matching indptr); VOID = unvisited.
    #
    # dfs_stack[]: DFS path; the sign bit encodes the "lead" flag
    #     (non-negative = candidate SCC root).
    #
    # comp_stack[]: nodes popped from DFS but not yet assigned to an SCC.

    cdef vector[int32_or_int64] succ_pos
    cdef vector[int] dfs_stack
    cdef vector[int] comp_stack
    succ_pos.assign(N, VOID)
    dfs_stack.reserve(N)
    comp_stack.reserve(N)

    labels[:] = 0

    for v in range(N):
        if succ_pos[v] != VOID:
            continue                       # already visited

        # ---- Init: new DFS tree rooted at v ----
        root_hl = index

        # ---- Previsit v ----
        dfs_stack.push_back(v)             # lead = True (non-negative)
        labels[v] = index
        index -= 1
        succ_pos[v] = indptr[v]

        while not dfs_stack.empty():
            v = _dfs_node(dfs_stack.back())

            if succ_pos[v] < indptr[v + 1]:
                # ---- Advance to next successor ----
                w = indices[succ_pos[v]]
                succ_pos[v] += 1

                if succ_pos[w] == VOID:
                    # ---- Previsit w (tree arc) ----
                    dfs_stack.push_back(w)             # lead = True (non-negative)
                    labels[w] = index
                    index -= 1
                    succ_pos[w] = indptr[w]
                else:
                    # ---- Revisit (cross / back arc) ----
                    if labels[v] < labels[w]:
                        dfs_stack[dfs_stack.size() - 1] = v | NONLEAD
                        labels[v] = labels[w]
                        # Early exit: the whole graph is one SCC
                        if labels[v] == root_hl and index == 0:
                            labels[v] = n_comp
                            for j in range(<int>comp_stack.size()):
                                labels[comp_stack[j]] = n_comp
                            for j in range(<int>dfs_stack.size() - 1):
                                labels[_dfs_node(dfs_stack[j])] = n_comp
                            n_comp += 1
                            dfs_stack.clear()
                            comp_stack.clear()
                            done = True
                            break
            else:
                # ---- Postvisit v ----
                if dfs_stack.back() >= 0:
                    # v is an SCC root: pop its members from comp stack
                    dfs_stack.pop_back()
                    while not comp_stack.empty():
                        top = comp_stack.back()
                        if labels[v] < labels[top]:
                            break
                        labels[top] = n_comp
                        comp_stack.pop_back()
                        index += 1
                    labels[v] = n_comp
                    index += 1
                    n_comp += 1
                else:
                    # Not a root: push v onto comp stack, propagate
                    dfs_stack.pop_back()
                    comp_stack.push_back(v)
                    if not dfs_stack.empty():
                        parent = _dfs_node(dfs_stack.back())
                        if labels[parent] < labels[v]:
                            dfs_stack[dfs_stack.size() - 1] = parent | NONLEAD
                            labels[parent] = labels[v]
        if done:
            break

    return n_comp


def _connected_components_undirected(
        np.ndarray[int32_or_int64, ndim=1, mode='c'] indices1,
        np.ndarray[int32_or_int64, ndim=1, mode='c'] indptr1,
        np.ndarray[int32_or_int64_b, ndim=1, mode='c'] indices2,
        np.ndarray[int32_or_int64_b, ndim=1, mode='c'] indptr2,
        np.ndarray[ITYPE_t, ndim=1, mode='c'] labels):
    return _connected_components_undirected2(indices1, indptr1,
                                             indices2, indptr2,
                                             labels)


cdef int _connected_components_undirected2(
        np.ndarray[int32_or_int64, ndim=1, mode='c'] indices1,
        np.ndarray[int32_or_int64, ndim=1, mode='c'] indptr1,
        np.ndarray[int32_or_int64_b, ndim=1, mode='c'] indices2,
        np.ndarray[int32_or_int64_b, ndim=1, mode='c'] indptr2,
        np.ndarray[ITYPE_t, ndim=1, mode='c'] labels) noexcept:

    cdef int v, w, j, label, SS_head
    cdef int N = labels.shape[0]
    DEF VOID = -1
    DEF END = -2
    labels.fill(VOID)
    label = 0

    # Share memory for the stack and labels, since labels are only
    # applied once a node has been popped from the stack.
    cdef np.ndarray[ITYPE_t, ndim=1, mode="c"] SS = labels
    SS_head = END
    for v in range(N):
        if labels[v] == VOID:
            # SS.push(v)
            SS_head = v
            SS[v] = END

            while SS_head != END:
                # v = SS.pop()
                v = SS_head
                SS_head = SS[v]

                labels[v] = label

                # Push children onto the stack if they haven't been
                # seen at all yet.
                for j in range(indptr1[v], indptr1[v+1]):
                    w = indices1[j]
                    if SS[w] == VOID:
                        SS[w] = SS_head
                        SS_head = w
                for j in range(indptr2[v], indptr2[v+1]):
                    w = indices2[j]
                    if SS[w] == VOID:
                        SS[w] = SS_head
                        SS_head = w
            label += 1

    return label
