"""
Packet Switching Network Simulation
====================================
Demonstrates how a message is split into packets, routed through
a 5-node network via different paths, and reassembled at the destination.

Nodes:  SRC --> A --> B --> C --> DST
                |              ^
                +--> D --> E --+

Packet routes (fixed, reproducible):
  Packet 1: SRC -> A -> B -> C -> DST
  Packet 2: SRC -> A -> D -> E -> DST
  Packet 3: SRC -> A -> B -> C -> DST  (same as P1, arrives later)
  Packet 4: SRC -> A -> D -> E -> DST  (same as P2, arrives later)

Packets travel one hop per frame. They arrive out of order at DST,
then are reassembled into the original sequence.
"""

import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import matplotlib.patheffects as pe
from matplotlib.animation import FuncAnimation
from matplotlib.lines import Line2D
import numpy as np

# ── Layout ──────────────────────────────────────────────────────────────────

NODE_POS = {
    "SRC": (0.05, 0.50),
    "A":   (0.25, 0.50),
    "B":   (0.50, 0.72),
    "C":   (0.75, 0.72),
    "D":   (0.50, 0.28),
    "E":   (0.75, 0.28),
    "DST": (0.95, 0.50),
}

EDGES = [
    ("SRC", "A"),
    ("A",   "B"),
    ("A",   "D"),
    ("B",   "C"),
    ("D",   "E"),
    ("C",   "DST"),
    ("E",   "DST"),
]

# ── Packet definitions ───────────────────────────────────────────────────────
# Each packet: id, data label, route, color, departure_frame (stagger starts)

PACKETS = [
    {"id": 1, "label": 'P1:"Hello"',   "route": ["SRC","A","B","C","DST"], "color": "#E63946", "start": 0},
    {"id": 2, "label": 'P2:" Wor"',    "route": ["SRC","A","D","E","DST"], "color": "#457B9D", "start": 1},
    {"id": 3, "label": 'P3:"ld! "',    "route": ["SRC","A","B","C","DST"], "color": "#2A9D8F", "start": 3},
    {"id": 4, "label": 'P4:":-) "',    "route": ["SRC","A","D","E","DST"], "color": "#E9C46A", "start": 2},
]

TOTAL_HOPS  = 4          # hops per packet (SRC→A→?→?→DST)
PAUSE_END   = 8          # extra frames to show reassembly
TOTAL_FRAMES = TOTAL_HOPS + max(p["start"] for p in PACKETS) + PAUSE_END

# ── Helpers ──────────────────────────────────────────────────────────────────

def lerp(a, b, t):
    return (a[0] + (b[0]-a[0])*t, a[1] + (b[1]-a[1])*t)


def packet_position(packet, frame):
    """Return (x,y) or None if not yet launched / already arrived."""
    local = frame - packet["start"]
    if local < 0:
        return None                         # not launched yet
    route = packet["route"]
    hop   = local                           # integer hop index
    if hop >= len(route) - 1:
        return None                         # arrived
    t = (frame - packet["start"] - hop)    # fractional progress within hop
    # We move one full hop per frame; t is always 0 here since we step by 1
    # For smooth interpolation use sub-frames; we animate at hop boundaries
    return NODE_POS[route[hop]]


def packet_arrived(packet, frame):
    local = frame - packet["start"]
    return local >= len(packet["route"]) - 1


def arrival_order(frame):
    """Return list of packet ids that have arrived, in arrival order."""
    arrivals = []
    for p in PACKETS:
        hop_count = len(p["route"]) - 1
        arrival_frame = p["start"] + hop_count
        if frame >= arrival_frame:
            arrivals.append((arrival_frame, p["id"]))
    arrivals.sort()
    return [pid for _, pid in arrivals]


# ── Figure setup ─────────────────────────────────────────────────────────────

fig, ax = plt.subplots(figsize=(13, 7))
fig.patch.set_facecolor("#0D1B2A")
ax.set_facecolor("#0D1B2A")
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
ax.set_aspect("equal")
ax.axis("off")

# Title
ax.text(0.5, 0.96, "Packet Switching Network Simulation",
        ha="center", va="top", fontsize=14, fontweight="bold",
        color="white", transform=ax.transAxes,
        fontfamily="monospace")

# Sub-title / legend strip
legend_text = "  ".join(
    f"[{p['id']}] {p['label']}" for p in PACKETS
)
ax.text(0.5, 0.02, legend_text,
        ha="center", va="bottom", fontsize=8,
        color="#AAAAAA", transform=ax.transAxes,
        fontfamily="monospace")

# ── Draw static edges ────────────────────────────────────────────────────────

for (n1, n2) in EDGES:
    x1, y1 = NODE_POS[n1]
    x2, y2 = NODE_POS[n2]
    ax.plot([x1, x2], [y1, y2], color="#2E4057", linewidth=2.5, zorder=1)

# ── Draw static nodes ────────────────────────────────────────────────────────

NODE_RADIUS = 0.045
node_circles = {}
node_labels  = {}

for name, (nx, ny) in NODE_POS.items():
    is_endpoint = name in ("SRC", "DST")
    color  = "#1A3A5C" if not is_endpoint else "#1D3557"
    border = "#4FC3F7" if not is_endpoint else "#E63946"
    circle = plt.Circle((nx, ny), NODE_RADIUS,
                         color=color, ec=border, linewidth=2.5, zorder=2)
    ax.add_patch(circle)
    node_circles[name] = circle

    display = {"SRC": "SRC\n(Sender)", "DST": "DST\n(Receiver)"}.get(name, name)
    yoff = -0.09 if name not in ("B","D") else (-0.09 if name == "D" else 0.09)
    ax.text(nx, ny + yoff, display,
            ha="center", va="center", fontsize=8,
            color="white", fontfamily="monospace", zorder=5)

# ── Packet dot artists ───────────────────────────────────────────────────────

dot_artists  = []
dot_labels   = []
trail_lines  = []

for p in PACKETS:
    dot = plt.Circle((0, 0), 0.025, color=p["color"], zorder=6, visible=False)
    ax.add_patch(dot)
    dot_artists.append(dot)

    lbl = ax.text(0, 0, str(p["id"]),
                  ha="center", va="center", fontsize=7,
                  color="black", fontweight="bold",
                  fontfamily="monospace", zorder=7, visible=False)
    dot_labels.append(lbl)

# ── DST receive buffer display ───────────────────────────────────────────────

buf_title = ax.text(0.95, 0.18, "Received:", ha="center", va="top",
                    fontsize=8, color="#AAAAAA", fontfamily="monospace", zorder=6)
buf_text  = ax.text(0.95, 0.13, "", ha="center", va="top",
                    fontsize=8, color="white", fontfamily="monospace", zorder=6)

reasm_text = ax.text(0.5, 0.08, "", ha="center", va="center",
                     fontsize=11, color="#2A9D8F", fontweight="bold",
                     fontfamily="monospace", zorder=6)

# Frame counter
frame_counter = ax.text(0.01, 0.01, "", ha="left", va="bottom",
                         fontsize=8, color="#555555",
                         fontfamily="monospace", zorder=6,
                         transform=ax.transAxes)

# ── Animation update ─────────────────────────────────────────────────────────

def update(frame):
    for i, p in enumerate(PACKETS):
        local = frame - p["start"]
        route = p["route"]

        if local < 0:
            # Not launched yet
            dot_artists[i].set_visible(False)
            dot_labels[i].set_visible(False)

        elif local >= len(route) - 1:
            # Arrived at DST — snap to DST, hide dot
            dot_artists[i].set_visible(False)
            dot_labels[i].set_visible(False)

        else:
            # In transit — position at current node (hop boundary)
            hop = local
            nx, ny = NODE_POS[route[hop]]
            dot_artists[i].center = (nx, ny)
            dot_artists[i].set_visible(True)
            dot_labels[i].set_position((nx, ny))
            dot_labels[i].set_visible(True)

    # ── Buffer / reassembly display ──
    arrived = arrival_order(frame)
    color_map = {p["id"]: p["color"] for p in PACKETS}
    label_map = {p["id"]: p["label"] for p in PACKETS}

    if arrived:
        buf_lines = "\n".join(
            f"  {label_map[pid]}" for pid in arrived
        )
        buf_text.set_text(buf_lines)
    else:
        buf_text.set_text("")

    if len(arrived) == 4:
        # All arrived — show reassembled message
        sorted_ids = sorted(arrived)
        msg_parts  = [label_map[pid].split('"')[1] for pid in sorted_ids]
        reasm_text.set_text('Reassembled: "' + "".join(msg_parts) + '"')
    else:
        reasm_text.set_text("")

    frame_counter.set_text(f"frame {frame}/{TOTAL_FRAMES-1}")

    return dot_artists + dot_labels + [buf_text, reasm_text, frame_counter]


# ── Slow enough to read ───────────────────────────────────────────────────────

ani = FuncAnimation(fig, update,
                    frames=TOTAL_FRAMES,
                    interval=900,          # ms per frame
                    blit=False,
                    repeat=True)

plt.tight_layout()
plt.show()
