#!/usr/bin/env python3
import subprocess
from collections import defaultdict, deque
from concurrent.futures import ThreadPoolExecutor
import threading
import os
import sys
import webbrowser
import urllib.request
MERMAID_TEMPLATE = """
    
    
    
    
    
    
"""
def ensure_local_files():
    """Ensure Mermaid.js and Panzoom.js are available locally."""
    js_files = {
        "mermaid.min.js": "https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js",
        "panzoom.min.js": "https://cdn.jsdelivr.net/npm/panzoom@9.4.3/dist/panzoom.min.js",
    }
    tmp_dir = "/tmp"
    
    for filename, url in js_files.items():
        file_path = os.path.join(tmp_dir, filename)
        if not os.path.exists(file_path):
            print(f"Downloading {filename}...")
            urllib.request.urlretrieve(url, file_path)
def preview_mermaid_graph(file_path):
    """Preview Mermaid graph in a browser."""
    ensure_local_files()
    
    if not os.path.exists(file_path):
        print(f"File {file_path} does not exist.")
        return
    # Load the Mermaid graph content
    with open(file_path, "r") as f:
        graph_content = f.read()
    # Prepare the HTML file content
    html_content = MERMAID_TEMPLATE.format(graph_content=graph_content)
    # Write the HTML to a temporary file
    html_file_path = f"{file_path}.html"
    with open(html_file_path, "w") as html_file:
        html_file.write(html_content)
    # Copy JS files to the same directory
    for js_file in ["mermaid.min.js", "panzoom.min.js"]:
        src = os.path.join("/tmp", js_file)
        dst = os.path.join(os.path.dirname(html_file_path), js_file)
        if not os.path.exists(dst):
            subprocess.run(["cp", src, dst])
    # Open the HTML file in the default web browser
    print(f"Opening {html_file_path} in the browser...")
    webbrowser.open(f"file://{os.path.abspath(html_file_path)}", new=2)
def list_installed_packages():
    """Retrieve a list of all installed packages."""
    result = subprocess.run(
        ["dpkg-query", "-f", "${binary:Package}\n", "-W"],
        stdout=subprocess.PIPE,
        text=True
    )
    return result.stdout.strip().split("\n")
def get_package_dependencies(package):
    """Query the direct dependencies of a single package."""
    result = subprocess.run(
        ["apt-cache", "depends", package],
        stdout=subprocess.PIPE,
        text=True
    )
    dependencies = []
    for line in result.stdout.strip().split("\n"):
        if line.strip().startswith("Depends:"):
            dep = line.split(":", 1)[1].strip()
            dep = dep.split(":")[0]  # Remove parts after colon
            dependencies.append(dep)
    return dependencies
def build_dependency_graph(packages):
    """Build a dependency graph for the packages and save it to a file."""
    graph = defaultdict(list)
    lock = threading.Lock()
    def process_package(package):
        dependencies = get_package_dependencies(package)
        with lock:
            graph[package].extend(dependencies)
    total_packages = len(packages)
    with ThreadPoolExecutor(max_workers=20) as executor:
        for i, _ in enumerate(executor.map(process_package, packages), start=1):
            progress = (i / total_packages) * 100
            print(f"Building dependency graph... {progress:.2f}% completed", end="\r")
    output_path = "/tmp/pkg.txt"
    with open(output_path, "w") as f:
        for package, dependencies in graph.items():
            for dep in dependencies:
                f.write(f"{package}-->{dep}\n")
    print(f"\nDependency graph built and saved to {output_path}")
def load_dependency_graph(file_path="/tmp/pkg.txt"):
    """Load the dependency graph from a file."""
    if not os.path.exists(file_path):
        raise FileNotFoundError(f"File {file_path} does not exist. Please run the build mode first.")
    graph = defaultdict(list)
    reverse_graph = defaultdict(list)
    with open(file_path, "r") as f:
        for line in f:
            line = line.strip()
            if "-->" in line:
                source, target = line.split("-->")
                graph[source].append(target)
                reverse_graph[target].append(source)
    return graph, reverse_graph
def trim_package_name(package):
    """Trim package name to conform to Mermaid syntax."""
    return package.replace("-", "_").replace(".", "_").replace("+", "_").replace(":", "_").replace("<", "_").replace(">", "_")
def generate_mermaid_graph(graph, root_package, exclude_leaves=False):
    """Generate a Mermaid diagram syntax for the graph."""
    lines = ["stateDiagram-v2"]
    visited = set()
    queue = deque([root_package])
    is_leaf = lambda pkg: len(graph.get(pkg, [])) == 0  # Determine if it is a leaf node
    while queue:
        package = queue.popleft()
        if package in visited:
            continue
        visited.add(package)
        dependencies = graph.get(package, [])
        for dep in dependencies:
            if exclude_leaves and is_leaf(dep):
                continue  # Skip leaf nodes
            lines.append(f"    {trim_package_name(package)} --> {trim_package_name(dep)}")
            if dep not in visited:
                queue.append(dep)
    return "\n".join(lines)
def build_mode():
    print("Retrieving installed packages...")
    packages = list_installed_packages()
    print("Building dependency graph...")
    build_dependency_graph(packages)
def depends_mode(package, exclude_leaves):
    graph, _ = load_dependency_graph()
    if package not in graph:
        print(f"Package {package} is not in the dependency graph.")
        return
    print("Generating dependency graph...")
    mermaid_graph = generate_mermaid_graph(graph, package, exclude_leaves)
    output_file = f"{package}_depends.mmd"
    with open(output_file, "w") as f:
        f.write("---\n")
        f.write(f"title: {package} Dependency Graph\n")
        f.write("---\n\n")
        f.write(mermaid_graph)
    print(f"Dependency graph generated and saved as {output_file}")
    preview_mermaid_graph(output_file)
def rdepends_mode(package, exclude_leaves):
    _, reverse_graph = load_dependency_graph()
    if package not in reverse_graph:
        print(f"Package {package} is not in the reverse dependency graph.")
        return
    print("Generating reverse dependency graph...")
    mermaid_graph = generate_mermaid_graph(reverse_graph, package, exclude_leaves)
    output_file = f"{package}_rdepends.mmd"
    with open(output_file, "w") as f:
        f.write("---\n")
        f.write(f"title: {package} Reverse Dependency Graph\n")
        f.write("---\n\n")
        f.write(mermaid_graph)
    print(f"Reverse dependency graph generated and saved as {output_file}")
    preview_mermaid_graph(output_file)
def main():
    if len(sys.argv) < 2:
        print("Usage: ./vispkg.py [build|depends|rdepends] [package] [--no-leaves]")
        sys.exit(1)
    mode = sys.argv[1]
    exclude_leaves = "--no-leaves" in sys.argv
    if mode == "build":
        build_mode()
    elif mode == "depends":
        if len(sys.argv) < 3:
            print("Usage: ./vispkg.py depends  [--no-leaves]")
            sys.exit(1)
        depends_mode(sys.argv[2], exclude_leaves)
    elif mode == "rdepends":
        if len(sys.argv) < 3:
            print("Usage: ./vispkg.py rdepends  [--no-leaves]")
            sys.exit(1)
        rdepends_mode(sys.argv[2], exclude_leaves)
    else:
        print("Unknown mode. Please use: build, depends, or rdepends.")
        sys.exit(1)
if __name__ == "__main__":
    main()