import warnings
import numpy as np
import scipy.sparse as sp

try:
    from ripser import ripser
    RIPSER_AVAILABLE = True
except ImportError:
    RIPSER_AVAILABLE = False

def _check_ripser_available():
    if not RIPSER_AVAILABLE:
        raise ImportError(
            "The 'ripser' library is required for TDA features. "
            "Please install it using `pip install ripser`."
        )

def graph_to_sparse_distance_matrix(graph, weight_attr='weight', invert_method='reciprocal'):
    """
    Convert an igraph.Graph to a scipy sparse distance matrix suitable for TDA.
    
    Args:
        graph: igraph.Graph object
        weight_attr: name of the weight attribute
        invert_method: 'reciprocal' (1/w) or 'max_minus' (max - w) or None (treat weights as distances)
    
    Returns:
        scipy.sparse.coo_matrix: Sparse distance matrix
    """
    if 'weight' not in graph.es.attributes():
        # Unweighted graph: distance is 1 for edges, infinity (handled by sparsity) for others
        # Rips works on distance matrix. 
        # For unweighted graph, typical distance is geodesic (shortest path) or just adjacency (1 for connected)
        # For Rips filtration usually we just provide the adjacency matrix as distance matrix?
        # If A_ij = 1, distance is 1. If A_ij = 0, distance is inf.
        # Rips complex adds edges at distance threshold.
        # So we return the adjacency matrix.
        adj = graph.get_adjacency_sparse()
        # For ripser with distance_matrix=True, 0 usually means distance 0 (same point).
        # We need to be careful. Rips on graphs usually means Metric Graph or adjacency as distance.
        # Here we assume edges represent "connectedness" at dist 1.
        return adj.astype(float)
    
    # Get weights
    weights = np.array(graph.es[weight_attr])
    
    # Invert weights to distances
    if invert_method == 'reciprocal':
        # Avoid division by zero
        distances = 1.0 / (weights + 1e-6)
    elif invert_method == 'max_minus':
        distances = (np.max(weights) + 1e-6) - weights
    elif invert_method is None:
        distances = weights
    else:
        raise ValueError(f"Unknown invert_method: {invert_method}")

    # Build sparse matrix
    # igraph edge indices
    edges = graph.get_edgelist()
    rows, cols = zip(*edges)
    
    # Create symmetric matrix if undirected
    if not graph.is_directed():
        rows_all = rows + cols
        cols_all = cols + rows
        data_all = np.concatenate([distances, distances])
    else:
        # TDA on directed graphs often treats them as undirected or uses specific directed TDA (not standard ripser)
        # Rips usually implies metric space -> symmetric.
        warnings.warn("TDA (Rips) typically expects undirected metric spaces. Treating graph as undirected for distance matrix.")
        rows_all = rows + cols
        cols_all = cols + rows
        data_all = np.concatenate([distances, distances])

    # n_vertices = graph.vcount()
    # sparse_dist = sp.coo_matrix((data_all, (rows_all, cols_all)), shape=(n_vertices, n_vertices))
    
    # Rips expects a distance matrix. 
    # Ideally, self-distance is 0. 
    # Missing entries in sparse matrix are effectively infinity for Rips if we specify it?
    # Ripsier documentation: "If a sparse matrix is passed, it is interpreted as a distance matrix... Note that 0s in the sparse matrix are treated as actual 0s (points on top of each other), whereas non-entries are treated as infinity."
    # So we simply return the matrix of edge distances.
    
    n_vertices = graph.vcount()
    return sp.coo_matrix((data_all, (rows_all, cols_all)), shape=(n_vertices, n_vertices))


def graph_persistence_diagram(graph, maxdim=1, weight_attr='weight', invert_method='reciprocal'):
    """
    Compute persistence diagram for a single graph.
    
    Args:
        graph: igraph.Graph
        maxdim: Maximum homology dimension (0 for H0, 1 for H1, etc.)
        weight_attr: Edge attribute for weight
        invert_method: How to convert weights to distances ('reciprocal', 'max_minus', or None)
        
    Returns:
        list of numpy arrays: [H0_diagram, H1_diagram, ...]
        Each diagram is an (N, 2) array of (birth, death) pairs.
    """
    _check_ripser_available()
    
    if graph.vcount() == 0:
        return [np.zeros((0, 2)) for _ in range(maxdim + 1)]

    # Prepare distance matrix
    sparse_dm = graph_to_sparse_distance_matrix(graph, weight_attr, invert_method)
    
    # Run ripser
    # distance_matrix=True checks if input is distance matrix
    # metric="precomputed" is implied by sparse matrix input, but we set distance_matrix=True explicitly
    result = ripser(sparse_dm, maxdim=maxdim, distance_matrix=True)
    
    return result['dgms']
