"""
Graph Algorithms Implementation with Pandas CSV Input
======================================================

This file contains implementations of various graph traversal and search algorithms:
- Depth-First Search (DFS)
- Breadth-First Search (BFS)
- Best-First Search (Greedy)
- A* Search Algorithm
- Reading graph data from CSV using Pandas

"""

import pandas as pd
import networkx as nx
from queue import PriorityQueue
import heapq

# =============================================================================
# READING GRAPH FROM CSV USING PANDAS
# =============================================================================

def read_graph_from_csv(csv_file):
    """
    Read graph data from a CSV file using pandas.
    
    CSV format should have columns: source, target, weight
    Example:
        source,target,weight
        A,B,1
        A,C,4
        B,D,5
    
    Args:
        csv_file (str): Path to the CSV file
    
    Returns:
        nx.Graph: NetworkX graph object
    """
    df = pd.read_csv(csv_file)
    G = nx.Graph()
    
    for _, row in df.iterrows():
        G.add_edge(row["source"], row["target"], weight=row["weight"])
    
    return G, df


# =============================================================================
# DEPTH-FIRST SEARCH (DFS)
# =============================================================================

def dfs_recursive(graph, node, visited=None):
    """
    Perform Depth-First Search recursively.
    
    Args:
        graph: NetworkX graph
        node: Starting node
        visited: Set of visited nodes
    
    Returns:
        list: Nodes in DFS order
    """
    if visited is None:
        visited = set()
    
    visited.add(node)
    dfs_order = [node]
    
    for neighbor in graph.neighbors(node):
        if neighbor not in visited:
            dfs_order.extend(dfs_recursive(graph, neighbor, visited))
    
    return dfs_order


def dfs_path(graph, start, goal):
    """
    Find a path from start to goal using DFS.
    
    Args:
        graph: NetworkX graph
        start: Starting node
        goal: Goal node
    
    Returns:
        list: Path from start to goal, or empty list if no path exists
    """
    visited = set()
    path = []
    
    def dfs_helper(current):
        visited.add(current)
        path.append(current)
        
        if current == goal:
            return True
        
        for neighbor in graph.neighbors(current):
            if neighbor not in visited:
                if dfs_helper(neighbor):
                    return True
        
        path.pop()
        return False
    
    dfs_helper(start)
    return path


# =============================================================================
# BREADTH-FIRST SEARCH (BFS)
# =============================================================================

def bfs_traversal(graph, start):
    """
    Perform Breadth-First Search traversal.
    
    Args:
        graph: NetworkX graph
        start: Starting node
    
    Returns:
        list: Nodes in BFS order
    """
    visited = set()
    queue = [start]
    visited.add(start)
    bfs_order = []
    
    while queue:
        node = queue.pop(0)
        bfs_order.append(node)
        
        for neighbor in graph.neighbors(node):
            if neighbor not in visited:
                visited.add(neighbor)
                queue.append(neighbor)
    
    return bfs_order


def bfs_shortest_path(graph, start, goal):
    """
    Find shortest path from start to goal using BFS.
    
    Args:
        graph: NetworkX graph
        start: Starting node
        goal: Goal node
    
    Returns:
        list: Shortest path from start to goal
    """
    visited = set()
    queue = [(start, [start])]
    visited.add(start)
    
    while queue:
        node, path = queue.pop(0)
        
        if node == goal:
            return path
        
        for neighbor in graph.neighbors(node):
            if neighbor not in visited:
                visited.add(neighbor)
                queue.append((neighbor, path + [neighbor]))
    
    return []


# =============================================================================
# BEST-FIRST SEARCH (GREEDY)
# =============================================================================

def best_first_search(graph, start, goal, heuristic):
    """
    Perform Best-First Search (Greedy search using heuristic only).
    
    Args:
        graph: NetworkX graph
        start: Starting node
        goal: Goal node
        heuristic: Dictionary mapping nodes to heuristic values
    
    Returns:
        list: Path from start to goal
    """
    visited = set()
    priority_queue = [(heuristic[start], start, [start])]
    
    while priority_queue:
        _, node, path = heapq.heappop(priority_queue)
        
        if node in visited:
            continue
        
        visited.add(node)
        
        if node == goal:
            return path
        
        for neighbor in graph.neighbors(node):
            if neighbor not in visited:
                new_path = path + [neighbor]
                priority = heuristic[neighbor]
                heapq.heappush(priority_queue, (priority, neighbor, new_path))
    
    return []


# =============================================================================
# A* SEARCH ALGORITHM
# =============================================================================

def astar_search(graph, start, goal, heuristic):
    """
    Perform A* search algorithm.
    A* uses f(n) = g(n) + h(n) where:
    - g(n) is the cost from start to node n
    - h(n) is the heuristic estimate from node n to goal
    
    Args:
        graph: NetworkX graph with edge weights
        start: Starting node
        goal: Goal node
        heuristic: Dictionary mapping nodes to heuristic values
    
    Returns:
        tuple: (path, cost) - Path from start to goal and total cost
    """
    visited = set()
    # Priority queue: (f_score, g_score, node, path)
    priority_queue = [(heuristic[start], 0, start, [start])]
    
    while priority_queue:
        f_score, g_score, node, path = heapq.heappop(priority_queue)
        
        if node in visited:
            continue
        
        visited.add(node)
        
        if node == goal:
            return path, g_score
        
        for neighbor in graph.neighbors(node):
            if neighbor not in visited:
                edge_weight = graph[node][neighbor].get('weight', 1)
                new_g_score = g_score + edge_weight
                new_f_score = new_g_score + heuristic[neighbor]
                new_path = path + [neighbor]
                heapq.heappush(priority_queue, (new_f_score, new_g_score, neighbor, new_path))
    
    return [], float('inf')


# =============================================================================
# EXAMPLE USAGE
# =============================================================================

if __name__ == "__main__":
    # Example: Create a sample CSV and run algorithms
    
    # Sample graph data
    sample_data = {
        'source': ['A', 'A', 'B', 'B', 'C'],
        'target': ['B', 'C', 'C', 'D', 'D'],
        'weight': [1, 4, 2, 5, 1]
    }
    
    df = pd.DataFrame(sample_data)
    print("Graph Data:")
    print(df)
    print("\n" + "="*60 + "\n")
    
    # Create graph
    G = nx.Graph()
    for _, row in df.iterrows():
        G.add_edge(row['source'], row['target'], weight=row['weight'])
    
    # Heuristic values (estimated distance to goal D)
    heuristic = {'A': 3, 'B': 2, 'C': 1, 'D': 0}
    
    # Run algorithms
    print("DFS Traversal from A:", dfs_recursive(G, 'A'))
    print("DFS Path from A to D:", dfs_path(G, 'A', 'D'))
    print("\n" + "-"*60 + "\n")
    
    print("BFS Traversal from A:", bfs_traversal(G, 'A'))
    print("BFS Shortest Path A to D:", bfs_shortest_path(G, 'A', 'D'))
    print("\n" + "-"*60 + "\n")
    
    print("Best-First Search A to D:", best_first_search(G, 'A', 'D', heuristic))
    print("\n" + "-"*60 + "\n")
    
    path, cost = astar_search(G, 'A', 'D', heuristic)
    print(f"A* Search A to D: {path} (Cost: {cost})")
