#!/usr/bin/env python3
"""Simulador de ventana deslizante: Go-Back-N y Selective Repeat, de libro.

    python3 window_sim.py traza gbn --window 4 --packets 6 --drop-data 2
    python3 window_sim.py traza sr --window 4 --packets 6 --drop-data 2
    python3 window_sim.py util --mbps 1000 --rtt 30 --bytes 1500
    python3 window_sim.py barrido
    python3 window_sim.py ambiguedad

Trabaja con paquetes numerados, sin bytes, sin conexión y con un temporizador fijo. Los
ACK son de libro: "ACK n" confirma el paquete n (en Go-Back-N, hasta el n inclusive). TCP
usa otra convención: su ACK dice qué byte espera a continuación.

Modelo: el emisor transmite un paquete a la vez, a la velocidad del enlace (d milisegundos
por paquete), y cada sentido demora rtt/2. Los ACK no ocupan tiempo de transmisión. El
enlace no tiene cola ni otros flujos: aquí no hay congestión.
"""
import argparse
import heapq
import random
from dataclasses import dataclass, field


@dataclass
class Config:
    protocol: str  # "gbn" o "sr"
    window: int
    packets: int
    d: float = 1.0  # tiempo de transmisión de un paquete, en ms
    rtt: float = 10.0
    timeout: float | None = None  # si no se da: el doble de lo que tarda una ventana en volver
    modulus: int | None = None  # espacio de números de secuencia; None es sin límite
    drop_data: list = field(default_factory=list)  # (paquete, copia), con copia desde 1
    drop_ack: list = field(default_factory=list)  # posición entre los ACK enviados, desde 1
    loss_data: float = 0.0
    loss_ack: float = 0.0
    seed: int = 1
    max_time: float = 10_000_000.0

    def timer(self):
        return self.timeout if self.timeout else 2 * (self.rtt + self.window * self.d)


@dataclass
class Stats:
    delivered: list = field(default_factory=list)
    transmissions: int = 0
    retransmissions: int = 0
    finish_time: float = 0.0
    finished: bool = False
    acks_sent: int = 0

    def utilization(self, d):
        return len(self.delivered) * d / self.finish_time if self.finish_time else 0.0

    def per_packet(self):
        return self.transmissions / len(self.delivered) if self.delivered else float("inf")


class WindowSim:
    """El emisor y el receptor de Go-Back-N o de Selective Repeat."""

    def __init__(self, config, trace=None):
        self.c = config
        self.trace = trace
        self.random = random.Random(config.seed)
        self.stats = Stats()
        self.events = []
        self.order = 0
        self.now = 0.0
        self.tx_free = 0.0
        self.copies = {}
        self.base = 0  # índice del paquete más antiguo sin confirmar
        self.next = 0
        self.acked = set()
        self.timers = {}  # generación del temporizador: uno por paquete en SR, uno solo en GBN
        self.expected = 0  # receptor, como número de secuencia
        self.rcv_buffer = {}  # receptor de SR: número de secuencia -> contenido

    # Ayudas ----------------------------------------------------------------------------
    def wire(self, index):
        return index % self.c.modulus if self.c.modulus else index

    def space(self, value):
        return value % self.c.modulus if self.c.modulus else value

    def log(self, side, text):
        if self.trace is not None:
            self.trace.append((self.now, side, text))

    def schedule(self, when, action, *args):
        heapq.heappush(self.events, (when, self.order, action, args))
        self.order += 1

    # Emisor ----------------------------------------------------------------------------
    def send(self, index, again=False):
        copy = self.copies.get(index, 0) + 1
        self.copies[index] = copy
        self.stats.transmissions += 1
        if again:
            self.stats.retransmissions += 1
        self.tx_free = max(self.tx_free, self.now) + self.c.d
        lost = (index, copy) in self.c.drop_data or self.random.random() < self.c.loss_data
        note = "  [se pierde]" if lost else ""
        word = "repite" if again else "envía"
        self.log("emisor", f"{word} {self.wire(index)}{note}")
        if not lost:
            self.schedule(self.tx_free + self.c.rtt / 2, self.at_receiver, self.wire(index), index)
        if self.c.protocol == "sr":
            self.arm_timer(index)

    def fill_window(self):
        while self.next < self.c.packets and self.next < self.base + self.c.window:
            first_in_flight = self.next == self.base
            self.send(self.next)
            if self.c.protocol == "gbn" and first_in_flight:
                self.arm_timer("gbn")
            self.next += 1

    def arm_timer(self, key):
        generation = self.timers.get(key, 0) + 1
        self.timers[key] = generation
        self.schedule(self.tx_free + self.c.timer(), self.on_timeout, key, generation)

    def stop_timer(self, key):
        self.timers[key] = self.timers.get(key, 0) + 1

    def on_timeout(self, key, generation):
        if self.timers.get(key) != generation or self.stats.finished:
            return
        if self.c.protocol == "gbn":
            self.log("emisor", f"vence el temporizador: repite desde {self.wire(self.base)}")
            for index in range(self.base, self.next):
                self.send(index, again=True)
            self.arm_timer("gbn")
        else:
            self.log("emisor", f"vence el temporizador de {self.wire(key)}")
            self.send(key, again=True)

    def at_sender(self, ack):
        for index in range(self.base, self.next):
            if self.wire(index) == ack:
                break
        else:
            self.log("emisor", f"recibe ACK {ack}: ya confirmado, lo ignora")
            return
        if self.c.protocol == "gbn":
            self.log("emisor", f"recibe ACK {ack}: confirma hasta {ack}")
            self.base = index + 1
            if self.base == self.next:
                self.stop_timer("gbn")
            else:
                self.arm_timer("gbn")
        else:
            self.log("emisor", f"recibe ACK {ack}")
            self.acked.add(index)
            self.stop_timer(index)
            while self.base in self.acked:
                self.base += 1
        if self.base >= self.c.packets:
            self.stats.finished = True
            self.stats.finish_time = self.now
            return
        self.fill_window()

    # Receptor --------------------------------------------------------------------------
    def send_ack(self, ack):
        self.stats.acks_sent += 1
        lost = self.stats.acks_sent in self.c.drop_ack or self.random.random() < self.c.loss_ack
        self.log("receptor", f"  ACK {ack}" + ("  [se pierde]" if lost else ""))
        if not lost:
            self.schedule(self.now + self.c.rtt / 2, self.at_sender, ack)

    def at_receiver(self, seq, content):
        if self.c.protocol == "gbn":
            self.gbn_receive(seq, content)
        else:
            self.sr_receive(seq, content)

    def gbn_receive(self, seq, content):
        if seq == self.expected:
            self.stats.delivered.append(content)
            self.log("receptor", f"recibe {seq}, entrega el contenido {content}")
            self.expected = self.space(self.expected + 1)
            self.send_ack(seq)
            return
        last_good = self.space(self.expected - 1)
        if self.stats.delivered:
            self.log("receptor", f"recibe {seq}: esperaba {self.expected}, lo descarta")
            self.send_ack(last_good)
        else:
            self.log("receptor", f"recibe {seq}: esperaba {self.expected}, lo descarta (nada que confirmar)")

    def sr_receive(self, seq, content):
        window, modulus = self.c.window, self.c.modulus
        offset = (seq - self.expected) % modulus if modulus else seq - self.expected
        if 0 <= offset < window:
            self.rcv_buffer[seq] = content
            released = []
            next_expected = self.expected
            while next_expected in self.rcv_buffer:
                released.append(self.rcv_buffer.pop(next_expected))
                next_expected = self.space(next_expected + 1)
            self.expected = next_expected
            self.stats.delivered += released
            what = f"entrega el contenido {released}" if released else "lo guarda"
            self.log("receptor", f"recibe {seq}, {what}")
            self.send_ack(seq)
            return
        behind = (self.expected - seq) % modulus if modulus else self.expected - seq
        if 0 < behind <= window:
            self.log("receptor", f"recibe {seq}: ya entregado, solo confirma")
            self.send_ack(seq)
        else:
            self.log("receptor", f"recibe {seq}: fuera de ventana, lo ignora")

    # Simulación ------------------------------------------------------------------------
    def run(self):
        self.fill_window()
        while self.events and not self.stats.finished:
            self.now, _, action, args = heapq.heappop(self.events)
            if self.now > self.c.max_time:
                break
            action(*args)
        if not self.stats.finished:
            self.stats.finish_time = self.now
        return self.stats


def simulate(**kwargs):
    return WindowSim(Config(**kwargs)).run()


def stop_and_wait_utilization(d, rtt):
    """La fórmula: un paquete cada d + rtt."""
    return d / (rtt + d)


def utilization(window, d, rtt):
    """Con ventana W y sin pérdidas: W paquetes cada d + rtt, sin pasar de 1."""
    return min(1.0, window * d / (rtt + d))


def window_to_fill(d, rtt):
    """La ventana mínima para llenar el enlace: (d + rtt) / d, redondeando hacia arriba."""
    needed = (rtt + d) / d
    whole = int(needed)
    return whole if abs(needed - whole) < 1e-9 else whole + 1


def sweep(window=50, packets=2000, d=1.0, rtt=40.0, seeds=5, losses=(0, 0.001, 0.01, 0.05, 0.1, 0.2),
          ack_only=False, timeout=None):
    """GBN contra SR, promediando varias semillas. Devuelve una lista de filas."""
    rows = []
    for loss in losses:
        row = {"loss": loss}
        for protocol in ("gbn", "sr"):
            per_packet = util = 0.0
            for seed in range(1, seeds + 1):
                kwargs = {"loss_ack": loss} if ack_only else {"loss_data": loss, "loss_ack": loss}
                stats = simulate(protocol=protocol, window=window, packets=packets, d=d, rtt=rtt,
                                 seed=seed, timeout=timeout, **kwargs)
                per_packet += stats.per_packet()
                util += stats.utilization(d)
            row[protocol] = (per_packet / seeds, util / seeds)
        rows.append(row)
    return rows


# Escenarios de ambigüedad: la primera ronda llega bien y se pierden todos sus ACK. El emisor
# repite el paquete 0, y el receptor ya está esperando números que dan la vuelta.
def ambiguity_run(protocol, modulus, window, packets=8):
    trace = []
    acks_lost = list(range(1, window + 1))
    stats = WindowSim(Config(protocol=protocol, window=window, packets=packets, d=1.0, rtt=10.0,
                             modulus=modulus, drop_ack=acks_lost, max_time=5000.0), trace).run()
    return stats, trace


def print_trace(trace):
    for when, side, text in trace:
        print(f"{when:7.1f}  {side:<9}{text}")


def command_trace(args):
    trace = []
    drop_data = [(index, 1) for index in args.drop_data]
    stats = WindowSim(Config(protocol=args.protocol, window=args.window, packets=args.packets,
                             d=args.d, rtt=args.rtt, timeout=args.timeout, drop_data=drop_data,
                             drop_ack=args.drop_ack), trace).run()
    print_trace(trace)
    print(f"entregado: {stats.delivered}   transmisiones: {stats.transmissions} "
          f"(repetidas: {stats.retransmissions})   tiempo: {stats.finish_time:.1f} ms")


def command_utilization(args):
    d = args.bytes * 8 / (args.mbps * 1000)
    print(f"d = {args.bytes} bytes a {args.mbps:g} Mbps = {d:.4f} ms    RTT = {args.rtt:g} ms")
    print(f"fórmula, stop-and-wait: U = d / (RTT + d) = "
          f"{stop_and_wait_utilization(d, args.rtt):.5f}")
    packets = args.packets
    stats = simulate(protocol="gbn", window=1, packets=packets, d=d, rtt=args.rtt)
    print(f"simulador, ventana 1:   U = {stats.utilization(d):.5f}")
    needed = window_to_fill(d, args.rtt)
    print(f"ventana para llenar el enlace: (RTT + d) / d = {needed} paquetes")
    print(f"throughput de stop-and-wait: {args.bytes * 8 / ((args.rtt + d) * 1000):.3f} Mbps")
    print()
    print(f"{'ventana':>8}{'fórmula':>10}{'simulador':>11}")
    for window in args.windows or [1, 10, 100, needed // 2, needed, needed * 2]:
        # Muchas ventanas: la fórmula es de régimen, y el comienzo y el final no aportan.
        count = max(packets, window * 40)
        measured = simulate(protocol="gbn", window=window, packets=count, d=d, rtt=args.rtt)
        print(f"{window:>8}{utilization(window, d, args.rtt):>10.4f}"
              f"{measured.utilization(d):>11.4f}")


def print_sweep(rows):
    print(f"{'pérdida':>8} | {'GBN envíos/paq':>14}{'GBN U':>8} | {'SR envíos/paq':>14}{'SR U':>8}")
    for row in rows:
        gbn, sr = row["gbn"], row["sr"]
        print(f"{row['loss'] * 100:7.1f}% | {gbn[0]:>14.2f}{gbn[1]:>8.2f} | {sr[0]:>14.2f}{sr[1]:>8.2f}")


def command_sweep(args):
    timer = args.timeout if args.timeout else 2 * (args.rtt + args.window * args.d)
    print(f"ventana {args.window}, {args.packets} paquetes, d = {args.d:g} ms, RTT = {args.rtt:g} ms, "
          f"temporizador = {timer:g} ms, promedio de {args.seeds} semillas")
    print("pérdida en los dos sentidos:")
    print_sweep(sweep(args.window, args.packets, args.d, args.rtt, args.seeds, timeout=args.timeout))
    print("pérdida solo en los ACK:")
    print_sweep(sweep(args.window, args.packets, args.d, args.rtt, args.seeds, ack_only=True,
                      timeout=args.timeout))


def command_ambiguity(args):
    for protocol, modulus, window in args.cases or [("sr", 4, 3), ("sr", 4, 2)]:
        stats, trace = ambiguity_run(protocol, modulus, window)
        print(f"--- {protocol}: {modulus} números, ventana {window}; "
              "se pierden los ACK de la primera ronda")
        print_trace(trace[:args.lines])
        if len(trace) > args.lines:
            print(f"        ... ({len(trace) - args.lines} líneas más)")
        expected = list(range(len(stats.delivered)))
        verdict = "correcto" if stats.delivered == expected and stats.finished else "INCORRECTO"
        print(f"contenidos entregados: {stats.delivered}   ({verdict})")
        print()


def main():
    parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    commands = parser.add_subparsers(dest="command", required=True)

    trace = commands.add_parser("traza")
    trace.add_argument("protocol", choices=["gbn", "sr"])
    trace.add_argument("--window", type=int, default=4)
    trace.add_argument("--packets", type=int, default=6)
    trace.add_argument("--d", type=float, default=1.0)
    trace.add_argument("--rtt", type=float, default=10.0)
    trace.add_argument("--timeout", type=float, default=None)
    trace.add_argument("--drop-data", type=int, nargs="*", default=[])
    trace.add_argument("--drop-ack", type=int, nargs="*", default=[])
    trace.set_defaults(run=command_trace)

    util = commands.add_parser("util")
    util.add_argument("--mbps", type=float, default=1000)
    util.add_argument("--rtt", type=float, default=30)
    util.add_argument("--bytes", type=int, default=1500)
    util.add_argument("--packets", type=int, default=200)
    util.add_argument("--windows", type=int, nargs="*", default=None)
    util.set_defaults(run=command_utilization)

    sweep_cmd = commands.add_parser("barrido")
    sweep_cmd.add_argument("--window", type=int, default=50)
    sweep_cmd.add_argument("--packets", type=int, default=2000)
    sweep_cmd.add_argument("--d", type=float, default=1.0)
    sweep_cmd.add_argument("--rtt", type=float, default=40.0)
    sweep_cmd.add_argument("--seeds", type=int, default=5)
    sweep_cmd.add_argument("--timeout", type=float, default=None)
    sweep_cmd.set_defaults(run=command_sweep)

    ambiguity = commands.add_parser("ambiguedad")
    ambiguity.add_argument("--lines", type=int, default=30)
    ambiguity.set_defaults(run=command_ambiguity, cases=None)

    args = parser.parse_args()
    args.run(args)


if __name__ == "__main__":
    main()
